Compare commits

...
This repository has been archived on 2023-08-08. You can view files and clone it, but cannot push or open issues or pull requests.

323 Commits

Author SHA1 Message Date
Sasha Koshka 501eb34922 Moved artist (now art) into another repo 2023-05-03 20:17:48 -04:00
Sasha Koshka 54ea1c283f Changed pkg.go.dev link 2023-05-03 20:12:46 -04:00
Sasha Koshka 33c787d70b Changed import paths 2023-05-03 19:40:30 -04:00
Sasha Koshka 794ab1b5e8 Slimmed down imports 2023-05-03 17:36:14 -04:00
Sasha Koshka 83d0b32fba No more defaultfont until we actually make one 2023-05-03 16:38:17 -04:00
Sasha Koshka ae12945676 Merge pull request 'reorganize' (#17) from reorganize into main
Reviewed-on: sashakoshka/tomo#17
2023-05-03 19:42:22 +00:00
Sasha Koshka f3185999a2 Merge branch 'reorganize' of git.tebibyte.media:sashakoshka/tomo into reorganize 2023-05-03 15:41:24 -04:00
Sasha Koshka 8ad14cd542 Updated readme 2023-05-03 15:40:40 -04:00
Sasha Koshka 8d587ae3b4 All examples work 2023-05-03 15:40:40 -04:00
Sasha Koshka 4f8469c359 Clipboard example works 2023-05-03 15:40:40 -04:00
Sasha Koshka 6e1369da5c The raycaster and piano examples would be better off in their own repo 2023-05-03 15:40:40 -04:00
Sasha Koshka abe63f4118 Migrated fun elements 2023-05-03 15:40:40 -04:00
Sasha Koshka cd6d8f3ff6 Remove redundant examples 2023-05-03 15:40:40 -04:00
Sasha Koshka 37df313544 ough 2023-05-03 15:40:40 -04:00
Sasha Koshka 2ac23185e6 Themes actuall get set now 2023-05-03 15:40:40 -04:00
Sasha Koshka 6241be1969 Ohg my god 2023-05-03 15:40:40 -04:00
Sasha Koshka 9588996bd8 Wintergreen is now a plugin 2023-05-03 15:40:40 -04:00
Sasha Koshka 2e3af402d5 Added script to install the X backend automatically 2023-05-03 15:40:40 -04:00
Sasha Koshka 69e73a7b84 Add gitignore 2023-05-03 15:40:40 -04:00
Sasha Koshka b84e444697 Updated X backend to match 2023-05-03 15:40:40 -04:00
Sasha Koshka b51eb79033 Entities must support all behaviors 2023-05-03 15:40:40 -04:00
Sasha Koshka 8e5ad8f385 Plugins are now properly loaded woohoo 2023-05-03 15:40:40 -04:00
Sasha Koshka b479ba8f0f Migrated test elements 2023-05-03 15:40:40 -04:00
Sasha Koshka 567358bf4c Made the X backend into a plugin 2023-05-03 15:40:40 -04:00
Sasha Koshka 10d5358390 Updated default theme 2023-05-03 15:40:40 -04:00
Sasha Koshka 09d360826b Yaeh 2023-05-03 15:40:40 -04:00
Sasha Koshka b3a9bba255 Added nasin documentation 2023-05-03 15:40:39 -04:00
Sasha Koshka 6acd8be05b Added Version type to base tomo package and stuff 2023-05-03 15:40:39 -04:00
Sasha Koshka 39b8b96513 Re-implemented removed functionality in Nasin
We also have a plugin system now :3
2023-05-03 15:40:39 -04:00
Sasha Koshka 363779a947 The base tomo module only retains a singleton backend 2023-05-03 15:40:39 -04:00
Sasha Koshka cd8371a3f3 Entities now give elements config and theme parameters 2023-05-03 15:40:39 -04:00
Sasha Koshka 4c9743387b Moved a lot of interfaces out of the base tomo module and into an
abilities module
2023-05-03 15:40:39 -04:00
Sasha Koshka e3d194562c Moved a bunch of code from artist into artutil 2023-05-03 15:40:39 -04:00
Sasha Koshka 7472cf52d9 Updated readme 2023-05-03 15:36:16 -04:00
Sasha Koshka 8d6deb3456 All examples work 2023-05-03 15:23:42 -04:00
Sasha Koshka cc03440125 Clipboard example works 2023-05-03 01:34:48 -04:00
Sasha Koshka cfdebddbdd The raycaster and piano examples would be better off in their own repo 2023-05-03 01:26:51 -04:00
Sasha Koshka 8a8e63ba05 Migrated fun elements 2023-05-03 01:18:10 -04:00
Sasha Koshka 6578480195 Remove redundant examples 2023-05-03 01:11:27 -04:00
Sasha Koshka 72fc28e223 ough 2023-05-03 01:07:44 -04:00
Sasha Koshka 9e754cdb59 Themes actuall get set now 2023-05-02 23:23:14 -04:00
Sasha Koshka fb25496f34 Ohg my god 2023-05-02 23:21:12 -04:00
Sasha Koshka 9cfbe0aa01 Wintergreen is now a plugin 2023-05-02 23:19:43 -04:00
Sasha Koshka e0be332953 Added script to install the X backend automatically 2023-05-02 22:25:54 -04:00
Sasha Koshka acc4148a18 Add gitignore 2023-05-02 22:19:29 -04:00
Sasha Koshka 3caa9c53ef Updated X backend to match 2023-05-02 22:18:34 -04:00
Sasha Koshka 4a723ff296 Entities must support all behaviors 2023-05-02 22:00:28 -04:00
Sasha Koshka de4bce8c46 Plugins are now properly loaded woohoo 2023-04-30 19:00:20 -04:00
Sasha Koshka ca27a810c7 Migrated test elements 2023-04-30 18:18:45 -04:00
Sasha Koshka 77e1151bd8 Made the X backend into a plugin 2023-04-30 14:16:14 -04:00
Sasha Koshka a2a9af3311 Updated default theme 2023-04-30 13:47:36 -04:00
Sasha Koshka 800ee2570f Yaeh 2023-04-30 13:45:21 -04:00
Sasha Koshka b92bdced9c Added nasin documentation 2023-04-30 13:31:23 -04:00
Sasha Koshka 85f995aa10 Added Version type to base tomo package and stuff 2023-04-30 13:05:17 -04:00
Sasha Koshka 7b7005c068 Re-implemented removed functionality in Nasin
We also have a plugin system now :3
2023-04-30 12:50:23 -04:00
Sasha Koshka d5d2cc1f4d The base tomo module only retains a singleton backend 2023-04-30 01:27:04 -04:00
Sasha Koshka dfdd721303 Entities now give elements config and theme parameters 2023-04-29 14:33:56 -04:00
Sasha Koshka fe3ac7ec1e Moved a lot of interfaces out of the base tomo module and into an
abilities module
2023-04-29 14:23:21 -04:00
Sasha Koshka f5180efc8a Moved a bunch of code from artist into artutil 2023-04-29 04:11:00 -04:00
Sasha Koshka e5d6e03975 X backend positions modals, panels, and menus correctly in reparenting window managers 2023-04-27 18:49:35 -04:00
Sasha Koshka 990e60eea4 Mouse events are no longer given to windows with a modal active 2023-04-27 00:05:29 -04:00
Sasha Koshka 4e20726eff Merge branch 'main' of git.tebibyte.media:sashakoshka/tomo 2023-04-26 14:00:02 -04:00
Sasha Koshka 3502da814d Add SelectionChange callback to list 2023-04-26 13:59:19 -04:00
Sasha Koshka 5407e52108 IconNone is now listed as an icon. 2023-04-25 18:00:16 -04:00
Sasha Koshka 7bb7111460 Added pin icons 2023-04-25 17:19:43 -04:00
Sasha Koshka e41fd63f35 Created a FlowList with similar properties to DocumentContainer 2023-04-21 22:24:28 -04:00
Sasha Koshka 58e02dced8 Fixed shapesColorLine
If the line started off-screen it would not draw. It draws now.
2023-04-21 21:49:29 -04:00
Sasha Koshka 61d3c14519 Fixed mouse test 2023-04-21 21:48:38 -04:00
Sasha Koshka cd7a683af9 Fixed the slider lol 2023-04-21 21:45:20 -04:00
Sasha Koshka e1156d65c8 ComboBox has arrow key support 2023-04-21 19:57:17 -04:00
Sasha Koshka fc4b2eb36d Tweaked list theming 2023-04-21 17:49:27 -04:00
Sasha Koshka 1c0dee1b95 Refined the combo box a bit 2023-04-21 17:29:56 -04:00
Sasha Koshka cc14151a14 Primitive combo box 2023-04-21 16:47:15 -04:00
Sasha Koshka 6622799019 Added a few context menus 2023-04-21 00:52:34 -04:00
Sasha Koshka f88268bb0e Updated default colors 2023-04-20 19:57:55 -04:00
Sasha Koshka c1046b1bcb Added more color definitions 2023-04-20 18:53:42 -04:00
Sasha Koshka 27799a9670 Improved switch styling 2023-04-20 18:50:28 -04:00
Sasha Koshka 2bd7d0fad5 Added a toggle button and lamp pattern 2023-04-20 18:40:05 -04:00
Sasha Koshka e5619ebf07 List has even more keynav support 2023-04-20 15:54:11 -04:00
Sasha Koshka 399dda75bd List now has keynav support 2023-04-20 15:09:51 -04:00
Sasha Koshka 53f78cb0e7 Overhauled mouse events
Everything gets an image.Point instead of an x y pair, and most
things now get modifiers.
2023-04-20 14:44:54 -04:00
Sasha Koshka eaee284aaf Lists are now single-column 2023-04-20 14:06:00 -04:00
Sasha Koshka 4fe778c095 Add notice to readme that the docs on pkg.go.dev might be old 2023-04-20 01:38:19 -04:00
Sasha Koshka d4b9ffb046 Improved documentation (and added missing methods) 2023-04-20 01:37:06 -04:00
Sasha Koshka 580b7d2ad0 The whole orientation thing was stupid 2023-04-20 01:10:47 -04:00
Sasha Koshka ff3802ca5e Forgot about sliders lol 2023-04-20 01:04:03 -04:00
Sasha Koshka 17fda82bbe Merge pull request 'I forgot to switch branches' (#16) from ecs into main
Reviewed-on: sashakoshka/tomo#16
2023-04-20 04:53:12 +00:00
Sasha Koshka 0063afed8c Made readme better 2023-04-20 00:52:24 -04:00
Sasha Koshka 1323a6c1ca Merge pull request 'ecs' (#15) from ecs into main
Reviewed-on: sashakoshka/tomo#15
2023-04-20 04:29:08 +00:00
Sasha Koshka 698414ee65 Raycaster example works 2023-04-20 00:22:29 -04:00
Sasha Koshka dbee2ff5a9 Directory view works 2023-04-20 00:15:37 -04:00
Sasha Koshka afdecc2c8b Containers now share a bunch of code 2023-04-19 00:29:25 -04:00
Sasha Koshka ac58a43220 Half-done implementation of file elements 2023-04-18 18:37:50 -04:00
Sasha Koshka 7cdc5868e5 Updated the examples 2023-04-18 16:18:30 -04:00
Sasha Koshka 14080b1f88 Element methods are now more consistent and have less bool flags
Still need to update most examples...
2023-04-18 13:14:10 -04:00
Sasha Koshka a2b1ac0c73 Oh yeah yeah! 2023-04-18 03:23:51 -04:00
Sasha Koshka 6276327613 Some theming tweaks 2023-04-18 03:12:36 -04:00
Sasha Koshka d44e7b51da Hehe 2023-04-18 03:08:28 -04:00
Sasha Koshka 785cc2d908 Child draw bounds are properly clipped 2023-04-18 03:07:06 -04:00
Sasha Koshka 0bf5c3b86c Lists are a thing now
Looks like child bounds arent clipped properly though, ugh
2023-04-18 02:59:44 -04:00
Sasha Koshka 6b13e772a9 Fixed segfault in the X backend when handling mouse motion 2023-04-17 02:16:27 -04:00
Sasha Koshka 427b5e025d Scroll now has a constructor similar to Cell 2023-04-17 02:13:21 -04:00
Sasha Koshka 5ca3b80e8e Made this crazy selection system 2023-04-17 02:05:53 -04:00
Sasha Koshka 775390e884 Containers are no longer in their own dir because why were they 2023-04-16 17:30:13 -04:00
Sasha Koshka a7de6c7f3b Document works now 2023-04-16 16:48:32 -04:00
Sasha Koshka 7d4ddaf387 Scrolling over a ScrollContainer will now scroll it 2023-04-16 14:12:55 -04:00
Sasha Koshka b9c8350677 Scroll containers yay 2023-04-16 03:37:28 -04:00
Sasha Koshka ed6de3a36f Got a bunch of examples working 2023-04-15 22:23:08 -04:00
Sasha Koshka e16195d274 The system can now focus previous, next 2023-04-15 21:49:40 -04:00
Sasha Koshka 0a21f605fb Added support for horizontal layouts 2023-04-15 19:14:44 -04:00
Sasha Koshka 0cd7fb9be9 Coherent commit messages are for weaklings 2023-04-15 18:51:42 -04:00
Sasha Koshka c0b205c6f0 This is what happens when you dont test anything oh my god 2023-04-15 18:49:02 -04:00
Sasha Koshka 1044c8299a Literally one set of parentheses 2023-04-15 18:33:37 -04:00
Sasha Koshka bb50c7d7a7 Lol 2023-04-15 18:30:22 -04:00
Sasha Koshka 9d78a599aa Migrated fun elements 2023-04-15 18:24:16 -04:00
Sasha Koshka 986315d5db Vertical layout partially works 2023-04-15 18:09:49 -04:00
Sasha Koshka 9e16f7b532 Migrated TextBox 2023-04-15 12:35:00 -04:00
Sasha Koshka ca86328506 Migrated some more elements 2023-04-15 01:45:11 -04:00
Sasha Koshka a43f5ce595 Window now checks for minimum size on adopt 2023-04-15 01:19:39 -04:00
Sasha Koshka 437aef0c27 Redid the entity system a bit to make it more reliable
Now it supports things like parenting elements before they are
added to a window and elements no longer have to constantly check
for a nil entity
2023-04-15 01:14:36 -04:00
Sasha Koshka 5cf0b162c0 Child property change events make more sense now 2023-04-15 00:02:30 -04:00
Sasha Koshka 6e4310b9ad Some X backend fixes 2023-04-14 23:58:14 -04:00
Sasha Koshka 68128c94d8 Migrated over some elements 2023-04-14 22:03:22 -04:00
Sasha Koshka 4c6f1f80e7 Proper keyboard and mouse event propagation 2023-04-14 19:08:14 -04:00
Sasha Koshka e931717241 Basic support in X backend for new API 2023-04-14 00:25:05 -04:00
Sasha Koshka bb9c5df088 X backend entity 2023-04-13 02:22:54 -04:00
Sasha Koshka 407b957687 Testing elements conform to new API 2023-04-12 23:46:29 -04:00
Sasha Koshka 99c890e6cd We won't be needing cores either 2023-04-12 23:25:40 -04:00
Sasha Koshka b190f01a71 It might be time to put layouts to bed 2023-04-12 23:25:08 -04:00
Sasha Koshka fa898be046 Updated the core tomo interfaces to support the ECS architecture 2023-04-12 23:21:34 -04:00
Sasha Koshka a51372bd7b ahhhhhhahhahahahahhh 2023-04-11 17:22:12 -04:00
Sasha Koshka 670cf36c14 Manually focusing the menu window messes everything up apparently 2023-04-10 18:11:40 -04:00
Sasha Koshka d67aac3d4f Menu windows actually work properly now 2023-04-10 18:07:49 -04:00
Sasha Koshka 2987331a31 Ok it kind of works now 2023-04-10 16:47:03 -04:00
Sasha Koshka da47026d1c Added untested support for OverrideRedirect windows 2023-04-10 16:22:47 -04:00
Sasha Koshka aed448671b Well I think thats all of the examples
There are too many examples.
2023-04-10 02:58:52 -04:00
Sasha Koshka 6db5901247 Added support for relative window positioning 2023-04-10 02:36:28 -04:00
Sasha Koshka 8abb45e77a Added a way to set WM_CLASS 2023-04-10 01:56:43 -04:00
Sasha Koshka d1fcc6e37f Older window managers will now understand the title 2023-04-09 01:57:56 -04:00
Sasha Koshka dc077a02ab Moved terminal stuff to a separate repository 2023-04-07 23:40:05 -04:00
Sasha Koshka 43a664009c End me 2023-04-07 23:03:42 -04:00
Sasha Koshka f21a41982e ANSI escape code decoder wip 2023-04-06 13:38:47 -04:00
Sasha Koshka 34b79ee30d Grid stub 2023-04-05 02:12:17 -04:00
Sasha Koshka 8db8fab14a No more stack overflow 2023-04-04 17:15:57 -04:00
Sasha Koshka cbdebc7f9f ScrollContainer can be controlled by page keys 2023-04-04 17:12:33 -04:00
Sasha Koshka 570853890e DocumentContainer now supports inlining elements 2023-04-04 16:39:12 -04:00
Sasha Koshka 260e2b31b6 Table now has keynav support 2023-04-04 15:05:26 -04:00
Sasha Koshka d633e0f5f6 Why wont the tecxt alighn ughghghgh 2023-04-04 13:44:38 -04:00
Sasha Koshka f377372354 Forgot to comment the table constructor 2023-04-03 23:10:39 -04:00
Sasha Koshka 55c13ebf89 TableContainer is now scrollable 2023-04-03 23:09:02 -04:00
Sasha Koshka eca75c642b Thats better 2023-04-03 22:36:37 -04:00
Sasha Koshka e38e2a47f9 Small theme tweaks 2023-04-03 22:31:34 -04:00
Sasha Koshka b357768c36 User can now select table cells 2023-04-03 22:22:29 -04:00
Sasha Koshka ebefcb03b3 Made table stretching slightly better 2023-04-03 21:48:57 -04:00
Sasha Koshka 13518d9ba6 Fixed fragmented/glitchy table drawing 2023-04-03 21:41:39 -04:00
Sasha Koshka ff51777834 Table's rebuildChildList method works properly 2023-04-03 20:06:17 -04:00
Sasha Koshka 941f6f6576 Added a (half-working) table element 2023-04-03 20:01:44 -04:00
Sasha Koshka 603d029c50 Fixed ProgressBar not having a minimum size 2023-04-03 16:12:53 -04:00
Sasha Koshka 5c2be06601 Upgraded xgbutil 2023-04-03 16:09:13 -04:00
Sasha Koshka 2d0a0cc073 Gave CoreControl the ability to shatter parent backgrounds 2023-04-02 22:46:38 -04:00
Sasha Koshka 46a4858597 Fixed the Texture pattern 2023-04-02 22:37:38 -04:00
Sasha Koshka 6c3230c0f8 Fixed CoreControl background drawing behavior
... But found a bug with the border pattern
2023-04-02 22:16:12 -04:00
Sasha Koshka 6ede0d0770 Added the BackgroundParent interface
Parents are now able to draw backgrounds for their children. This
means we can now have elements inside other elements that aren't
restricted to one background color.
2023-04-02 22:02:55 -04:00
Sasha Koshka 7521808872 Added table patterns 2023-04-02 21:15:16 -04:00
Sasha Koshka bc72333ff0 Formatting fixes 2023-04-02 19:01:06 -04:00
Sasha Koshka 7fee67474f Got rid of the "Invalid" state parameter
It was a bit too niche to be a state parameter
2023-04-02 18:57:29 -04:00
Sasha Koshka 9f70804420 Added a whole bunch of new icons 2023-04-02 17:55:24 -04:00
Sasha Koshka e9dff8ad07 Added more colors 2023-04-02 01:56:19 -04:00
Sasha Koshka bd636eaa7f Added defaultfont.Face
This will eventually completely replace basicfont. Need to design
a custom default Tomo font and implement a way to load from a
compressed binary format that will take up a very small amount of
room embedded into an executable.
2023-04-01 14:27:54 -04:00
Sasha Koshka 4e488582d0 Oh my god 2023-03-31 21:14:40 -04:00
Sasha Koshka b8bf5743b4 Artist test uses new defaultfont location 2023-03-31 21:11:10 -04:00
Sasha Koshka 8c03b516e3 TextBox has double-click to select word 2023-03-31 20:28:53 -04:00
Sasha Koshka 50d7d74097 Added documentaion comments for textmanip 2023-03-31 19:40:25 -04:00
Sasha Koshka 03dfcf02bf Added double click delay to config 2023-03-31 14:02:56 -04:00
Sasha Koshka c7cd944ae2 Removed redundant HandleWidth parameter from config
The handle width can be specified by themes with padding values.
This also allows for far more granularity of the handle width
adjustment as it can depend on context.
2023-03-31 13:55:45 -04:00
Sasha Koshka d1b5cd863a Added more package-level comments 2023-03-31 13:50:26 -04:00
Sasha Koshka e7ec9ad6f3 Moved defaultfont to default/font 2023-03-31 13:45:52 -04:00
Sasha Koshka c1e2bf46a6 TextBox supports copy/paste with keyboard commands 2023-03-31 03:25:46 -04:00
Sasha Koshka ab78bc640d Piano example no longer crashes 2023-03-31 01:30:18 -04:00
Sasha Koshka 7b300333cf I am going insane 2023-03-31 01:06:29 -04:00
Sasha Koshka 53bfc8df68 Re-organized module structure 2023-03-30 23:19:04 -04:00
Sasha Koshka 719b7b99ac Merge pull request 'clipboard' (#14) from clipboard into main
Reviewed-on: sashakoshka/tomo#14
2023-03-31 01:45:28 +00:00
Sasha Koshka e7ad588fb8 Apparently go mod replace can do this! 2023-03-30 21:37:57 -04:00
Sasha Koshka 6406b70077 Add cut capability to textmanip 2023-03-30 21:33:49 -04:00
Sasha Koshka 6456759bfc The targets list now has the proper type of ATOM 2023-03-30 20:51:11 -04:00
Sasha Koshka 0d4104255c Selection data is property sent to the requestor 2023-03-30 18:52:29 -04:00
Sasha Koshka 17422cc054 selectionClaim seeks to the start of the data before reading it 2023-03-30 18:42:40 -04:00
Sasha Koshka a16f3c2cd7 TARGETS list is now properly assembled 2023-03-30 18:32:14 -04:00
Sasha Koshka 017543aa0f Temporary redirect to patched xgbutil that will only work on my machine 2023-03-30 18:05:29 -04:00
Sasha Koshka f9e5503320 Pasting implemented (nonworking) 2023-03-30 13:10:58 -04:00
Sasha Koshka 8abc4defa7 Fixed INCR
Oops!
2023-03-29 23:24:42 -04:00
Sasha Koshka fc228a13d3 Fleshed out the mime type conversion method a bit 2023-03-29 12:33:57 -04:00
Sasha Koshka 1ebf5e1103 Implemented INCR selection properties 2023-03-29 12:27:23 -04:00
Sasha Koshka ab61615018 X backend generates mime type from owner response 2023-03-29 03:03:13 -04:00
Sasha Koshka 39dc09bc4a X backend clipboard properly negotiates data type with owner
The clipboard API has been changed to allow an application to
accept a number of different mime types, and the X backend will now
check the accepted types list against the owner's TARGETS list and
choose the best one.
2023-03-29 02:55:12 -04:00
Sasha Koshka 0aede3502b This should have been several separate commits 2023-03-29 00:50:23 -04:00
Sasha Koshka 6f15ff3366 We now set the target atom properly 2023-03-28 01:00:54 -04:00
Sasha Koshka 01a0fc1bd3 You can fcucking PASTE now!!! 2023-03-27 20:44:39 -04:00
Sasha Koshka 02a27447b9 Changed the clipboard API so that it will work with X
In X, clipboard/selection data is specific to each window, and it
may take some time before the clipboard data is fully transferred.
This actually makes sense because there can be entire images in
the clipboard and it is important the clipboard API supports large
file transfer. Because of this, the Copy and Paste methods have
been moved into Window, and Paste now returns a channel.
2023-03-25 13:32:48 -04:00
Sasha Koshka 6a3f45a2e0 Set transient for on panels
This makes panels behave as expected. It feels incredibly wrong but
shotcut does it, it can't be that bad.
2023-03-24 22:49:53 -04:00
Sasha Koshka 3aa8495873 Terrible discovery (panels don't work properly) 2023-03-24 17:38:21 -04:00
Sasha Koshka bdc1109bcf Modal dialogs lock the window's input until they are closed 2023-03-24 01:31:40 -04:00
Sasha Koshka a2c0ff5f4c Popups package uses the new modal system 2023-03-24 00:47:04 -04:00
Sasha Koshka d710d13f0d Added the ability to make different window types 2023-03-24 00:34:25 -04:00
Sasha Koshka fff5ad4d96 File now produces an error'd icon for an erroneous file 2023-03-23 20:57:51 -04:00
Sasha Koshka 8447b06641 Created a convenience constructor for Inset 2023-03-23 18:05:30 -04:00
Sasha Koshka 6a08d0f317 Added a Component parameter to theme cases 2023-03-23 17:34:08 -04:00
Sasha Koshka d3d3cddfef Merge pull request 'file-elements' (#13) from file-elements into main
Reviewed-on: sashakoshka/tomo#13
2023-03-23 20:21:36 +00:00
Sasha Koshka 45021b6153 Rename DirectoryView to Directory 2023-03-23 15:56:56 -04:00
Sasha Koshka 6638a471c7 File is now configurable 2023-03-23 15:55:18 -04:00
Sasha Koshka 6c8ff55dc1 Text labels are now drawn
The typesetter will need to be reworked to properly break lines in
the middle of words for this to function properly.
2023-03-23 15:38:51 -04:00
Sasha Koshka 7ec5e1ab2a Made the buttons in the file browser example work 2023-03-23 14:45:46 -04:00
Sasha Koshka 14802b4b82 Implemented history for DirectoryView
For some reason DirectoryView won't draw changes all of the time...
2023-03-23 14:37:44 -04:00
Sasha Koshka f74f6a43f8 DirectoryView selects and de-selects files 2023-03-23 14:11:42 -04:00
Sasha Koshka 68341517f7 DirectoryView uses File to display files 2023-03-21 18:03:31 -04:00
Sasha Koshka dcc672e2bc ScrollContainer does not scroll child in a forbidden direction 2023-03-21 17:37:33 -04:00
Sasha Koshka d9bddce20b File and directory view elements wip 2023-03-21 12:26:48 -04:00
Sasha Koshka 60aac053fb Add ability to change an icon's icon 2023-03-21 12:26:06 -04:00
Sasha Koshka faf5ebb283 List can now add multiple entries at once, and clear all of them 2023-03-20 01:57:06 -04:00
Sasha Koshka f37101eb9e Ctrl+a selects all in TextBox 2023-03-20 01:56:12 -04:00
Sasha Koshka d475e5e2ec TextBox now has an OnEnter method. 2023-03-20 01:13:23 -04:00
Sasha Koshka 221647a265 Made icons a bit better
There is a new IconNone and an IconUpward, and buttons don't
expand awkwardly when they have an icon.
2023-03-20 01:12:19 -04:00
Sasha Koshka bf667aded9 Small tweaks to the wintergreen atlas 2023-03-18 01:57:17 -04:00
Sasha Koshka b4befa5aa5 Made the default handle width an odd number 2023-03-17 02:05:22 -04:00
Sasha Koshka 0f272f4835 DocumentContainer does as well 2023-03-17 02:00:19 -04:00
Sasha Koshka d651570746 The list element calls the scroll bounds change callback 2023-03-17 01:58:42 -04:00
Sasha Koshka 493c5210a7 DocumentContainer sets minimum size properly 2023-03-17 01:52:26 -04:00
Sasha Koshka 0fd56f272c Fixed text being cut of on several examples 2023-03-17 01:38:57 -04:00
Sasha Koshka 4c6e01203c Label.EmCollapse actually works now 2023-03-17 01:00:11 -04:00
Sasha Koshka b189518c92 Did the same thing for the vertical layout 2023-03-16 23:04:33 -04:00
Sasha Koshka 4b788dd783 Horizontal layout does not use integer math anymore
Instead it uses fixed.Int26_6. Could have used floats but then we
would need a new point datatype and we already have utility
functions for fixed point math
2023-03-16 20:55:11 -04:00
Sasha Koshka cdf805dadc Implemented all text alignment methods 2023-03-16 20:24:33 -04:00
Sasha Koshka 6258c77f86 Added an align method to label 2023-03-16 15:58:26 -04:00
Sasha Koshka b90ffeb4fd X backend window disowns child before closing
This prevents elements from drawing to a closed window (causing
xgb to print an error)
2023-03-16 14:42:18 -04:00
Sasha Koshka a4ef28cdd0 Moved containers into a separate package 2023-03-16 14:22:56 -04:00
Sasha Koshka c55925d152 Added a package that just links all backends 2023-03-16 01:14:39 -04:00
Sasha Koshka 11b680db63 Added package summaries to more packages 2023-03-16 01:10:59 -04:00
Sasha Koshka d57db6327d Merge branch 'main' of git.tebibyte.media:sashakoshka/tomo 2023-03-16 00:45:50 -04:00
Sasha Koshka 0d8d2a0190 Cleaned up pattern documentation a bit 2023-03-16 00:45:33 -04:00
Sasha Koshka 6ee3014fda Merge pull request 'robust-parenting' (#12) from robust-parenting into main
Reviewed-on: sashakoshka/tomo#12
2023-03-16 04:34:08 +00:00
Sasha Koshka 0ebf0bc814 Raycaster example now works 2023-03-16 00:30:59 -04:00
Sasha Koshka 40aa1a788b Renamed some oddly named files 2023-03-16 00:26:54 -04:00
Sasha Koshka bffdb000ed Piano element handles motion events 2023-03-16 00:25:36 -04:00
Sasha Koshka 5ca9206f65 DocumentContainer properly adopts children now 2023-03-16 00:24:40 -04:00
Sasha Koshka 1239f4e03d Made DocumentContainer satisfy FlexibleParent 2023-03-15 23:57:22 -04:00
Sasha Koshka 8aaa017902 Re-added OnScrollBoundsChange methods because they are useful 2023-03-15 23:56:00 -04:00
Sasha Koshka 639baecee5 Propagator unfocuses children before focusing a new one 2023-03-15 23:49:57 -04:00
Sasha Koshka c1b3562d10 It compiles 2023-03-15 23:47:13 -04:00
Sasha Koshka ef325d5161 Found a flaw in the focusing model, rectifying.
Still need to fix on X backend window, that will be in the next
commit.
2023-03-15 17:08:43 -04:00
Sasha Koshka 2f60abdfa3 Core properly sets nil parent 2023-03-15 01:46:58 -04:00
Sasha Koshka 1a66224648 X backend window sets itself as parent (oops) 2023-03-15 01:43:32 -04:00
Sasha Koshka 275e113e3b Fun elements now conform to new API 2023-03-15 01:42:07 -04:00
Sasha Koshka 0015820fac Basic elements now conform to new API 2023-03-15 01:41:23 -04:00
Sasha Koshka f4799ba03d Testing elements now conform to new API 2023-03-14 19:41:36 -04:00
Sasha Koshka 14ad35d85c X backend now conforms to new API changes 2023-03-14 18:54:24 -04:00
Sasha Koshka a34e8768ab Redid cores to conform to the new API changes 2023-03-14 18:30:32 -04:00
Sasha Koshka b08cbea320 Overhauled the element interfaces
Instead of the previous parenting model where parents would set
child callbacks during adoption by probing for callback setters,
child elements will instead probe their parents for notify methods
listed in the standard parent interfaces. This means that an
element cannot be half-parented to something, nor can it be
parented to two things at once. Parent elements may themselves
fulfill these interfaces, or they can pass a hook that fulfills
them to the child.
2023-03-14 17:08:39 -04:00
Sasha Koshka 9d84c50db3 Merge pull request 'flexible-elements-were-a-mistake' (#11) from flexible-elements-were-a-mistake into main
Reviewed-on: sashakoshka/tomo#11
2023-03-14 03:37:58 +00:00
Sasha Koshka 99e029ae09 TextBox no longer aggressively requests focus 2023-03-13 22:25:57 -04:00
Sasha Koshka 5149c27cf3 Added untested label collapse 2023-03-13 17:10:27 -04:00
Sasha Koshka 7ef95cc751 Removed unneeded Container.reflectChildProperties() 2023-03-12 01:57:56 -05:00
Sasha Koshka b09994973c List and Piano do shattering properly 2023-03-12 01:47:58 -05:00
Sasha Koshka 37048c6759 Raycaster runs? 2023-03-12 01:33:05 -05:00
Sasha Koshka be45f7ad71 Fixed some artist bugs 2023-03-12 01:23:20 -05:00
Sasha Koshka c45268d8c1 Testing elements now conform to the new API 2023-03-12 01:19:40 -05:00
Sasha Koshka 92e5822185 Basic and fun elements conform to new API change 2023-03-12 01:15:36 -05:00
Sasha Koshka d31aee1ba8 X backend now follows API 2023-03-12 01:06:12 -05:00
Sasha Koshka 0f8affd2b2 Made similar changes to the Pattern interface and all of artist 2023-03-12 01:04:06 -05:00
Sasha Koshka 3d28ebe4cf Made interfacial changes that will allow for elements to be clipped 2023-03-12 00:17:35 -05:00
Sasha Koshka 5afbc0e713 DocumentContainer constrains its scroll position on resize 2023-03-11 20:04:08 -05:00
Sasha Koshka b7a7800370 DocumentContainer has a proper minimum width 2023-03-11 19:25:35 -05:00
Sasha Koshka 15fa3b2497 Quelled some of the strangeness 2023-03-11 18:27:16 -05:00
Sasha Koshka 081b005679 Added a somewhat buggy DocumentContainer 2023-03-11 18:00:29 -05:00
Sasha Koshka 1be769526d Removed references to flexible from containers 2023-03-11 00:48:15 -05:00
Sasha Koshka 51084a6cfe Removed references to flexible from layouts, x backend, core 2023-03-11 00:43:26 -05:00
Sasha Koshka 677dca1dbf ScrollContainer uses ScrollBar for scrolling 2023-03-11 00:21:54 -05:00
Sasha Koshka 9cc9e78504 Large icons in the default set! 2023-03-10 18:53:27 -05:00
Sasha Koshka 5d4a26a877 AnalogClock is no longer flexible. 2023-03-10 13:45:53 -05:00
Sasha Koshka aaa794ac04 ScrollBar handles scroll wheel events 2023-03-10 13:42:51 -05:00
Sasha Koshka 8658ecd879 Sort of fixed a flexible height bug 2023-03-10 00:10:26 -05:00
Sasha Koshka 1c28613981 The scroll bar is better 2023-03-09 23:27:08 -05:00
Sasha Koshka 8e1638e054 I may have fixed the wierd scrollbar rendering
And something else I didn't realize was there
2023-03-09 22:23:09 -05:00
Sasha Koshka aff9aca835 We now have an untested lone scrollbar element 2023-03-09 18:15:52 -05:00
Sasha Koshka cf672824a6 im dumb as hell bruh 2023-03-08 21:05:56 -05:00
Sasha Koshka 04884bd8e3 Oh my joodness 2023-03-08 20:41:48 -05:00
Sasha Koshka 305acea285 Use ezprof to profile 2023-03-08 20:24:43 -05:00
Sasha Koshka f3c1c95a57 Keyboard control for sliders 2023-03-07 19:13:08 -05:00
Sasha Koshka 423e6869c0 X backend better handles expose events
Previously, when an expose event was recieved, the backend would
call Window.paste, converting RGBA image data to BGRA image data.
Now we only call Window.pushRegion with the bounds given to us by
the expose event(s). This speeds up window resizing significantly.
2023-03-07 12:48:29 -05:00
Sasha Koshka 803812f9c9 Texture pattern now samples X position correctly 2023-03-06 21:40:20 -05:00
Sasha Koshka c171273240 Sped up rendering significantly 2023-03-06 21:34:14 -05:00
Sasha Koshka 11402cfc25 Button applies the sink offset correctly to icons 2023-03-05 11:20:33 -05:00
Sasha Koshka 7e0d64e8bd TextBox text is now vertically centered 2023-03-05 10:58:27 -05:00
Sasha Koshka d38bd1cbf5 Hiding button text actually works now 2023-03-05 00:31:41 -05:00
Sasha Koshka 865dd20724 Buttons can now hide their text 2023-03-05 00:23:45 -05:00
Sasha Koshka 6c46fc6f7c Made a few icons blacker 2023-03-05 00:07:37 -05:00
Sasha Koshka 0071994ba6 Buttons can now have icons 2023-03-05 00:05:56 -05:00
Sasha Koshka 11c747e225 Dialog boxes now have icons 2023-03-04 23:09:46 -05:00
Sasha Koshka 61bbe0e346 Added an Icon element 2023-03-04 22:56:44 -05:00
Sasha Koshka 1e8bb56b7c Added small icons to default theme 2023-03-04 22:07:59 -05:00
Sasha Koshka ae6cf128f8 Created Icon interface 2023-03-04 20:48:46 -05:00
Sasha Koshka ecaad02c0b Added a bunch of icon IDs 2023-03-04 20:10:53 -05:00
Sasha Koshka cad10a1fb1 Merge pull request 'make-containers-better' (#10) from make-containers-better into main
Reviewed-on: sashakoshka/tomo#10
2023-03-04 21:26:33 +00:00
Sasha Koshka 912a3f9f66 oops lmao 2023-03-04 16:18:43 -05:00
Sasha Koshka 531b0ffce9 Fixed Container not clearing child event handlers in DisownAll 2023-03-04 10:44:45 -05:00
Sasha Koshka 9c12cd7e18 Fixed cringe bug with focus requests being improperly handled 2023-03-04 02:20:48 -05:00
Sasha Koshka 4f6f4e1f1a Me when I make the exact mistake twice 2023-03-04 02:04:47 -05:00
Sasha Koshka dc5ddfc0bd Propagator no longer segfaults when handling keynav 2023-03-04 01:48:16 -05:00
Sasha Koshka 5fc5af92df Layouts now take in proper margin and padding values 2023-03-04 01:42:14 -05:00
Sasha Koshka 90ce0d7281 Fixed Propagator.forChildren 2023-03-04 01:29:45 -05:00
Sasha Koshka be286fa86c The container actually creates a propagator now lmao 2023-03-04 01:27:16 -05:00
Sasha Koshka 252433f13d Cleaned up Container somewhat 2023-03-04 01:26:23 -05:00
Sasha Koshka 165d0835bf Worked Propagator into basic.Container 2023-03-04 01:20:23 -05:00
Sasha Koshka 56e11ae1de Cleaned up the (ChildIterator -> Parent) interface 2023-03-04 01:05:37 -05:00
Sasha Koshka 1d9fb6024d Fully implemented Propagator 2023-03-04 00:57:17 -05:00
Sasha Koshka c13cdd570d Implemented all focus methods except for HandleFocus
I am dreading this
2023-03-04 00:38:37 -05:00
Sasha Koshka 5af8d7fd97 Implemented keyboard, mouse, theme, and config event propagation 2023-03-04 00:18:27 -05:00
Sasha Koshka b6eb158964 Tidied up documentation on Propagator 2023-03-03 23:48:10 -05:00
Sasha Koshka 6bb5b2d79c Created the stub for Propagator
Unlike the previous poorly-defined ContainerCore idea, this struct
has one sole responsibility and that is propagating events to
children. There may be another struct called like ChildManager or
something in the future that also abstracts away logic for
adoption, canvas cutting, disowning, layout, etc.
2023-03-03 20:31:30 -05:00
Sasha Koshka 538123dcd5 No that was a bad idea time to do something else 2023-03-03 20:16:36 -05:00
Sasha Koshka 38baa97e76 ContainerCore and ContainerCoreControl WIP 2023-03-02 18:59:08 -05:00
Sasha Koshka e9e6e4fbe7 Added padding/margin distinction to layouts 2023-03-02 17:58:42 -05:00
Sasha Koshka 285cb4810f Remove margin from layout interface
Layouts will need to store margin and padding values within
themseleves.
2023-03-02 16:48:37 -05:00
Sasha Koshka 2cac2b3bd0 Merge pull request 'data-oriented-patterns' (#9) from data-oriented-patterns into main
Reviewed-on: sashakoshka/tomo#9
2023-03-01 18:07:08 +00:00
170 changed files with 9777 additions and 9083 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -1,29 +1,24 @@
# ![tomo](assets/banner.png)
# ![tomo](assets/screenshot.png)
This repository is [mirrored on GitHub](https://github.com/sashakoshka/tomo).
Please note: Tomo is in early development. Some features may not work properly,
and its API may change without notice.
Tomo is a retro-looking GUI toolkit written in pure Go. It is designed with
these goals in mind:
Tomo is a GUI toolkit written in pure Go with minimal external dependencies. It
makes use of Go's unique language features to do more with less.
- Modularity: the core of Tomo is mostly composed of interfaces—and the
overwhelming majority of its code resides in pluggable modules. If you don't
need it, then dont import it—and you can be assured it won't be there.
- Extendability: during the design of Tomo's API, use cases such as creating
custom backends, elements, and layouts were given just as much importance as
normal application building. Your custom element is a first-class citizen.
- Independence: Tomo is minimally dependent on code outside of the Go
standard library. Because of this, the file size of a compiled Tomo application
is typically very small.
- Frugality: Tomo foregoes things like animations and anti-aliasing in order to
use a minimal amount of system resources without even having to touch the GPU.
- Consistency: Tomo's design is not only consistent within itself, but also
with the Go standard library. If you've worked with Go in the past, Tomo will
feel pleasantly familliar.
Nasin is an application framework that runs on top of Tomo. It supports plugins
which can extend any application with backends, themes, etc.
You can find out more about how to use it by visiting the examples directory,
or pull up its documentation by running `godoc` within the repository. You can
also view it on the web on
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/sashakoshka/tomo).
## Usage
Before you start using Tomo, you need to install a backend plugin. Currently,
there is only an X backend. You can run ./scripts/install-backends.sh to install
it. It will be placed in `~/.local/lib/nasin/plugins`.
You can find out more about how to use Tomo and Nasin by visiting the examples
directory, or pull up the documentation by running `godoc` within the
repository. You can also view it on the web on
[pkg.go.dev](https://pkg.go.dev/git.tebibyte.media/tomo/tomo) (although
it may be slightly out of date).

241
ability/element.go Normal file
View File

@ -0,0 +1,241 @@
// Package ability defines extended interfaces that elements can support.
package ability
import "image"
import "tomo"
import "tomo/input"
import "art"
// Layoutable represents an element that needs to perform layout calculations
// before it can draw itself.
type Layoutable interface {
tomo.Element
// Layout causes this element to perform a layout operation.
Layout ()
}
// Container represents an element capable of containing child elements.
type Container interface {
tomo.Element
Layoutable
// DrawBackground causes the element to draw its background pattern to
// the specified canvas. The bounds of this canvas specify the area that
// is actually drawn to, while the Entity bounds specify the actual area
// of the element.
DrawBackground (art.Canvas)
// HandleChildMinimumSizeChange is called when a child's minimum size is
// changed.
HandleChildMinimumSizeChange (child tomo.Element)
}
// Enableable represents an element that can be enabled and disabled. Disabled
// elements typically appear greyed out.
type Enableable interface {
tomo.Element
// Enabled returns whether or not the element is enabled.
Enabled () bool
// SetEnabled sets whether or not the element is enabled.
SetEnabled (bool)
}
// Focusable represents an element that has keyboard navigation support.
type Focusable interface {
tomo.Element
Enableable
// HandleFocusChange is called when the element is focused or unfocused.
HandleFocusChange ()
}
// Selectable represents an element that can be selected. This includes things
// like list items, files, etc. The difference between this and Focusable is
// that multiple Selectable elements may be selected at the same time, whereas
// only one Focusable element may be focused at the same time. Containers who's
// purpose is to contain selectable elements can determine when to select them
// by implementing MouseTargetContainer and listening for HandleChildMouseDown
// events.
type Selectable interface {
tomo.Element
Enableable
// HandleSelectionChange is called when the element is selected or
// deselected.
HandleSelectionChange ()
}
// KeyboardTarget represents an element that can receive keyboard input.
type KeyboardTarget interface {
tomo.Element
// HandleKeyDown is called when a key is pressed down or repeated while
// this element has keyboard focus. It is important to note that not
// every key down event is guaranteed to be paired with exactly one key
// up event. This is the reason a list of modifier keys held down at the
// time of the key press is given.
HandleKeyDown (key input.Key, modifiers input.Modifiers)
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
HandleKeyUp (key input.Key, modifiers input.Modifiers)
}
// MouseTarget represents an element that can receive mouse events.
type MouseTarget interface {
tomo.Element
// HandleMouseDown is called when a mouse button is pressed down on this
// element.
HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element.
HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers)
}
// MouseTargetContainer represents an element that wants to know when one
// of its children is clicked. Children do not have to implement MouseTarget for
// a container satisfying MouseTargetContainer to be notified that they have
// been clicked.
type MouseTargetContainer interface {
Container
// HandleMouseDown is called when a mouse button is pressed down on a
// child element.
HandleChildMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on a child element.
HandleChildMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element)
}
// MotionTarget represents an element that can receive mouse motion events.
type MotionTarget interface {
tomo.Element
// HandleMotion is called when the mouse is moved over this element,
// or the mouse is moving while being held down and originally pressed
// down on this element.
HandleMotion (position image.Point)
}
// ScrollTarget represents an element that can receive mouse scroll events.
type ScrollTarget interface {
tomo.Element
// HandleScroll is called when the mouse is scrolled. The X and Y
// direction of the scroll event are passed as deltaX and deltaY.
HandleScroll (
position image.Point,
deltaX, deltaY float64,
modifiers input.Modifiers)
}
// Flexible represents an element who's preferred minimum height can change in
// response to its width.
type Flexible interface {
tomo.Element
// FlexibleHeightFor returns what the element's minimum height would be
// if resized to a specified width. This does not actually alter the
// state of the element in any way, but it may perform significant work,
// so it should be called sparingly.
//
// It is reccomended that parent containers check for this interface and
// take this method's value into account in order to support things like
// flow layouts and text wrapping, but it is not absolutely necessary.
// The element's MinimumSize method will still return the absolute
// minimum size that the element may be resized to.
//
// It is important to note that if a parent container checks for
// flexible chilren, it itself will likely need to be either scrollable
// or flexible.
FlexibleHeightFor (width int) int
}
// FlexibleContainer represents an element that is capable of containing
// flexible children.
type FlexibleContainer interface {
Container
// HandleChildFlexibleHeightChange is called when the parameters
// affecting a child's flexible height are changed.
HandleChildFlexibleHeightChange (child Flexible)
}
// Scrollable represents an element that can be scrolled. It acts as a viewport
// through which its contents can be observed.
type Scrollable interface {
tomo.Element
// ScrollContentBounds returns the full content size of the element.
ScrollContentBounds () image.Rectangle
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
ScrollViewportBounds () image.Rectangle
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
ScrollTo (position image.Point)
// ScrollAxes returns the supported axes for scrolling.
ScrollAxes () (horizontal, vertical bool)
}
// ScrollableContainer represents an element that is capable of containing
// scrollable children.
type ScrollableContainer interface {
Container
// HandleChildScrollBoundsChange is called when the content bounds,
// viewport bounds, or scroll axes of a child are changed.
HandleChildScrollBoundsChange (child Scrollable)
}
// Collapsible represents an element who's minimum width and height can be
// manually resized. Scrollable elements should implement this if possible.
type Collapsible interface {
tomo.Element
// Collapse collapses the element's minimum width and height. A value of
// zero for either means that the element's normal value is used.
Collapse (width, height int)
}
// Themeable represents an element that can modify its appearance to fit within
// a theme.
type Themeable interface {
tomo.Element
// HandleThemeChange is called whenever the theme is changed.
HandleThemeChange ()
}
// Configurable represents an element that can modify its behavior to fit within
// a set of configuration parameters.
type Configurable interface {
tomo.Element
// HandleConfigChange is called whenever configuration parameters are
// changed.
HandleConfigChange ()
}

View File

@ -1,12 +0,0 @@
package artist
import "image/color"
// Hex creates a color.RGBA value from an RGBA integer value.
func Hex (color uint32) (c color.RGBA) {
c.A = uint8(color)
c.B = uint8(color >> 8)
c.G = uint8(color >> 16)
c.R = uint8(color >> 24)
return
}

View File

@ -1,2 +0,0 @@
// Package artist provides a simple 2D drawing library for canvas.Canvas.
package artist

View File

@ -1,60 +0,0 @@
package artist
import "image"
// Side represents one side of a rectangle.
type Side int; const (
SideTop Side = iota
SideRight
SideBottom
SideLeft
)
// Inset represents an inset amount for all four sides of a rectangle. The top
// side is at index zero, the right at index one, the bottom at index two, and
// the left at index three. These values may be negative.
type Inset [4]int
// Apply returns the given rectangle, shrunk on all four sides by the given
// inset. If a measurment of the inset is negative, that side will instead be
// expanded outward. If the rectangle's dimensions cannot be reduced any
// further, an empty rectangle near its center will be returned.
func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) {
smaller = bigger
if smaller.Dx() < inset[3] + inset[1] {
smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2
smaller.Max.X = smaller.Min.X
} else {
smaller.Min.X += inset[3]
smaller.Max.X -= inset[1]
}
if smaller.Dy() < inset[0] + inset[2] {
smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2
smaller.Max.Y = smaller.Min.Y
} else {
smaller.Min.Y += inset[0]
smaller.Max.Y -= inset[2]
}
return
}
// Inverse returns a negated version of the inset.
func (inset Inset) Inverse () (prime Inset) {
return Inset {
inset[0] * -1,
inset[1] * -1,
inset[2] * -1,
inset[3] * -1,
}
}
// Horizontal returns the sum of SideRight and SideLeft.
func (inset Inset) Horizontal () int {
return inset[SideRight] + inset[SideLeft]
}
// Vertical returns the sum of SideTop and SideBottom.
func (inset Inset) Vertical () int {
return inset[SideTop] + inset[SideBottom]
}

View File

@ -1,67 +0,0 @@
package artist
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
// Pattern is capable of drawing to a canvas within the bounds of a given
// clipping rectangle.
type Pattern interface {
// Draw draws to destination, using the bounds of destination as a width
// and height for things like gradients, bevels, etc. The pattern may
// not draw outside the union of destination.Bounds() and clip. The
// clipping rectangle effectively takes a subset of the pattern. To
// change the bounds of the pattern itself, use canvas.Cut() on the
// destination before passing it to Draw().
Draw (destination canvas.Canvas, clip image.Rectangle)
}
// Draw lets you use several clipping rectangles to draw a pattern.
func Draw (
destination canvas.Canvas,
source Pattern,
clips ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
for _, clip := range clips {
source.Draw(destination, clip)
updatedRegion = updatedRegion.Union(clip)
}
return
}
// DrawBounds lets you specify an overall bounding rectangle for drawing a
// pattern. The destination is cut to this rectangle.
func DrawBounds (
destination canvas.Canvas,
source Pattern,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
return Draw(canvas.Cut(destination, bounds), source, bounds)
}
// DrawShatter is like an inverse of Draw, drawing nothing in the areas
// specified in "rocks".
func DrawShatter (
destination canvas.Canvas,
source Pattern,
rocks ...image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
tiles := shatter.Shatter(destination.Bounds(), rocks...)
return Draw(destination, source, tiles...)
}
// AllocateSample returns a new canvas containing the result of a pattern. The
// resulting canvas can be sourced from shape drawing functions. I beg of you
// please do not call this every time you need to draw a shape with a pattern on
// it because that is horrible and cruel to the computer.
func AllocateSample (source Pattern, width, height int) (allocated canvas.Canvas) {
allocated = canvas.NewBasicCanvas(width, height)
source.Draw(allocated, allocated.Bounds())
return
}

View File

@ -1,90 +0,0 @@
package patterns
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Border is a pattern that behaves similarly to border-image in CSS. It divides
// a source canvas into nine sections...
//
// Inset[1]
// ┌──┴──┐
// ┌─┌─────┬─────┬─────┐
// Inset[0]─┤ │ 0 │ 1 │ 2 │
// └─├─────┼─────┼─────┤
// │ 3 │ 4 │ 5 │
// ├─────┼─────┼─────┤─┐
// │ 6 │ 7 │ 8 │ ├─Inset[2]
// └─────┴─────┴─────┘─┘
// └──┬──┘
// Inset[3]
//
// ... Where the bounds of section 4 are defined as the application of the
// pattern's inset to the canvas's bounds. The bounds of the other eight
// sections are automatically sized around it.
//
// When drawn to a destination canvas, the bounds of sections 1, 3, 4, 5, and 7
// are expanded or contracted to fit the destination's bounds. All sections
// are rendered as if they are Texture patterns, meaning these flexible sections
// will repeat to fill in any empty space.
//
// This pattern can be used to make a static image texture into something that
// responds well to being resized.
type Border struct {
canvas.Canvas
artist.Inset
}
// Draw draws the border pattern onto the destination canvas within the clipping
// bounds.
func (pattern Border) Draw (destination canvas.Canvas, clip image.Rectangle) {
bounds := clip.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return }
srcSections := nonasect(pattern.Bounds(), pattern.Inset)
srcTextures := [9]Texture { }
for index, section := range srcSections {
srcTextures[index].Canvas = canvas.Cut(pattern, section)
}
dstSections := nonasect(destination.Bounds(), pattern.Inset)
for index, section := range dstSections {
srcTextures[index].Draw(canvas.Cut(destination, section), clip)
}
}
func nonasect (bounds image.Rectangle, inset artist.Inset) [9]image.Rectangle {
center := inset.Apply(bounds)
return [9]image.Rectangle {
// top
image.Rectangle {
bounds.Min,
center.Min },
image.Rect (
center.Min.X, bounds.Min.Y,
center.Max.X, center.Min.Y),
image.Rect (
center.Max.X, bounds.Min.Y,
bounds.Max.X, center.Min.Y),
// center
image.Rect (
bounds.Min.X, center.Min.Y,
center.Min.X, center.Max.Y),
center,
image.Rect (
center.Max.X, center.Min.Y,
bounds.Max.X, center.Max.Y),
// bottom
image.Rect (
bounds.Min.X, center.Max.Y,
center.Min.X, bounds.Max.Y),
image.Rect (
center.Min.X, center.Max.Y,
center.Max.X, bounds.Max.Y),
image.Rect (
center.Max.X, center.Max.Y,
bounds.Max.X, bounds.Max.Y),
}
}

View File

@ -1,3 +0,0 @@
// Package patterns provides a basic set of types that satisfy the
// artist.Pattern interface.
package patterns

View File

@ -1,41 +0,0 @@
package patterns
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Texture is a pattern that tiles the content of a canvas both horizontally and
// vertically.
type Texture struct {
canvas.Canvas
}
// Draw tiles the pattern's canvas within the clipping bounds. The minimum
// points of the pattern's canvas and the destination canvas will be lined up.
func (pattern Texture) Draw (destination canvas.Canvas, clip image.Rectangle) {
realBounds := destination.Bounds()
bounds := clip.Canon().Intersect(realBounds)
if bounds.Empty() { return }
dstData, dstStride := destination.Buffer()
srcData, srcStride := pattern.Buffer()
srcBounds := pattern.Bounds()
point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
srcPoint := point.Sub(realBounds.Min).Add(srcBounds.Min)
dstIndex := point.X + point.Y * dstStride
srcIndex :=
wrap(srcPoint.X, srcBounds.Min.X, srcBounds.Max.X) +
wrap(srcPoint.Y, srcBounds.Min.Y, srcBounds.Max.Y) * srcStride
dstData[dstIndex] = srcData[srcIndex]
}}
}
func wrap (value, min, max int) int {
difference := max - min
value = (value - min) % difference
if value < 0 { value += difference }
return value + min
}

View File

@ -1,20 +0,0 @@
package patterns
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
// Uniform is a pattern that draws a solid color.
type Uniform color.RGBA
// Draw fills the clipping rectangle with the pattern's color.
func (pattern Uniform) Draw (destination canvas.Canvas, clip image.Rectangle) {
shapes.FillColorRectangle(destination, color.RGBA(pattern), clip)
}
// Uhex creates a new Uniform pattern from an RGBA integer value.
func Uhex (color uint32) (uniform Uniform) {
return Uniform(artist.Hex(color))
}

View File

@ -1,11 +0,0 @@
// Package shapes provides some basic shape drawing routines.
//
// A word about using patterns with shape routines:
//
// Most drawing routines have a version that samples from other canvases, and a
// version that samples from a solid color. None of these routines can use
// patterns directly, but it is entirely possible to have a pattern draw to an
// off-screen canvas and then draw a shape based on that canvas. As a little
// bonus, you can save the canvas for later so you don't have to render the
// pattern again when you need to redraw the shape.
package shapes

View File

@ -1,228 +0,0 @@
package shapes
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// TODO: redo fill ellipse, stroke ellipse, etc. so that it only takes in
// destination and source, using the bounds of destination as the bounds of the
// ellipse and the bounds of source as the "clipping rectangle". Line up the Min
// of both canvases.
func FillEllipse (
destination canvas.Canvas,
source canvas.Canvas,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds())
realBounds := destination.Bounds()
if bounds.Empty() { return }
updatedRegion = bounds
point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
if inEllipse(point, realBounds) {
offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
dstData[dstIndex] = srcData[srcIndex]
}
}}
return
}
func StrokeEllipse (
destination canvas.Canvas,
source canvas.Canvas,
weight int,
) {
if weight < 1 { return }
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
bounds := destination.Bounds().Inset(weight - 1)
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
realBounds := destination.Bounds()
if bounds.Empty() { return }
context := ellipsePlottingContext {
plottingContext: plottingContext {
dstData: dstData,
dstStride: dstStride,
srcData: srcData,
srcStride: srcStride,
weight: weight,
offset: offset,
bounds: realBounds,
},
radii: image.Pt(bounds.Dx() / 2, bounds.Dy() / 2),
}
context.center = bounds.Min.Add(context.radii)
context.plotEllipse()
}
type ellipsePlottingContext struct {
plottingContext
radii image.Point
center image.Point
}
func (context ellipsePlottingContext) plotEllipse () {
x := float64(0)
y := float64(context.radii.Y)
// region 1 decision parameter
decision1 :=
float64(context.radii.Y * context.radii.Y) -
float64(context.radii.X * context.radii.X * context.radii.Y) +
(0.25 * float64(context.radii.X) * float64(context.radii.X))
decisionX := float64(2 * context.radii.Y * context.radii.Y * int(x))
decisionY := float64(2 * context.radii.X * context.radii.X * int(y))
// draw region 1
for decisionX < decisionY {
points := []image.Point {
image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, int(y) + context.center.Y),
}
if context.srcData == nil {
context.plotColor(points[0])
context.plotColor(points[1])
context.plotColor(points[2])
context.plotColor(points[3])
} else {
context.plotSource(points[0])
context.plotSource(points[1])
context.plotSource(points[2])
context.plotSource(points[3])
}
if (decision1 < 0) {
x ++
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decision1 += decisionX + float64(context.radii.Y * context.radii.Y)
} else {
x ++
y --
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision1 +=
decisionX - decisionY +
float64(context.radii.Y * context.radii.Y)
}
}
// region 2 decision parameter
decision2 :=
float64(context.radii.Y * context.radii.Y) * (x + 0.5) * (x + 0.5) +
float64(context.radii.X * context.radii.X) * (y - 1) * (y - 1) -
float64(context.radii.X * context.radii.X * context.radii.Y * context.radii.Y)
// draw region 2
for y >= 0 {
points := []image.Point {
image.Pt( int(x) + context.center.X, int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, int(y) + context.center.Y),
image.Pt( int(x) + context.center.X, -int(y) + context.center.Y),
image.Pt(-int(x) + context.center.X, -int(y) + context.center.Y),
}
if context.srcData == nil {
context.plotColor(points[0])
context.plotColor(points[1])
context.plotColor(points[2])
context.plotColor(points[3])
} else {
context.plotSource(points[0])
context.plotSource(points[1])
context.plotSource(points[2])
context.plotSource(points[3])
}
if decision2 > 0 {
y --
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision2 += float64(context.radii.X * context.radii.X) - decisionY
} else {
y --
x ++
decisionX += float64(2 * context.radii.Y * context.radii.Y)
decisionY -= float64(2 * context.radii.X * context.radii.X)
decision2 +=
decisionX - decisionY +
float64(context.radii.X * context.radii.X)
}
}
}
// FillColorEllipse fills an ellipse within the destination canvas with a solid
// color.
func FillColorEllipse (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
realBounds := bounds
bounds = bounds.Intersect(destination.Bounds()).Canon()
if bounds.Empty() { return }
updatedRegion = bounds
point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
if inEllipse(point, realBounds) {
dstData[point.X + point.Y * dstStride] = color
}
}}
return
}
// StrokeColorEllipse is similar to FillColorEllipse, but it draws an inset
// outline of an ellipse instead.
func StrokeColorEllipse (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,
) (
updatedRegion image.Rectangle,
) {
if weight < 1 { return }
dstData, dstStride := destination.Buffer()
insetBounds := bounds.Inset(weight - 1)
context := ellipsePlottingContext {
plottingContext: plottingContext {
dstData: dstData,
dstStride: dstStride,
color: color,
weight: weight,
bounds: bounds.Intersect(destination.Bounds()),
},
radii: image.Pt(insetBounds.Dx() / 2, insetBounds.Dy() / 2),
}
context.center = insetBounds.Min.Add(context.radii)
context.plotEllipse()
return
}
func inEllipse (point image.Point, bounds image.Rectangle) bool {
point = point.Sub(bounds.Min)
x := (float64(point.X) + 0.5) / float64(bounds.Dx()) - 0.5
y := (float64(point.Y) + 0.5) / float64(bounds.Dy()) - 0.5
return math.Hypot(x, y) <= 0.5
}

View File

@ -1,112 +0,0 @@
package shapes
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// ColorLine draws a line from one point to another with the specified weight
// and color.
func ColorLine (
destination canvas.Canvas,
color color.RGBA,
weight int,
min image.Point,
max image.Point,
) (
updatedRegion image.Rectangle,
) {
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
updatedRegion.Max.X ++
updatedRegion.Max.Y ++
data, stride := destination.Buffer()
bounds := destination.Bounds()
context := linePlottingContext {
plottingContext: plottingContext {
dstData: data,
dstStride: stride,
color: color,
weight: weight,
bounds: bounds,
},
min: min,
max: max,
}
if abs(max.Y - min.Y) < abs(max.X - min.X) {
if max.X < min.X { context.swap() }
context.lineLow()
} else {
if max.Y < min.Y { context.swap() }
context.lineHigh()
}
return
}
type linePlottingContext struct {
plottingContext
min image.Point
max image.Point
}
func (context *linePlottingContext) swap () {
temp := context.max
context.max = context.min
context.min = temp
}
func (context linePlottingContext) lineLow () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
yi := 1
if deltaY < 0 {
yi = -1
deltaY *= -1
}
D := (2 * deltaY) - deltaX
point := context.min
for ; point.X < context.max.X; point.X ++ {
if !point.In(context.bounds) { break }
context.plotColor(point)
if D > 0 {
D += 2 * (deltaY - deltaX)
point.Y += yi
} else {
D += 2 * deltaY
}
}
}
func (context linePlottingContext) lineHigh () {
deltaX := context.max.X - context.min.X
deltaY := context.max.Y - context.min.Y
xi := 1
if deltaX < 0 {
xi = -1
deltaX *= -1
}
D := (2 * deltaX) - deltaY
point := context.min
for ; point.Y < context.max.Y; point.Y ++ {
if !point.In(context.bounds) { break }
context.plotColor(point)
if D > 0 {
point.X += xi
D += 2 * (deltaX - deltaY)
} else {
D += 2 * deltaX
}
}
}
func abs (n int) int {
if n < 0 { n *= -1}
return n
}

View File

@ -1,47 +0,0 @@
package shapes
import "image"
import "image/color"
// FIXME? drawing a ton of overlapping squares might be a bit wasteful.
type plottingContext struct {
dstData []color.RGBA
dstStride int
srcData []color.RGBA
srcStride int
color color.RGBA
weight int
offset image.Point
bounds image.Rectangle
}
func (context plottingContext) square (center image.Point) (square image.Rectangle) {
return image.Rect(0, 0, context.weight, context.weight).
Sub(image.Pt(context.weight / 2, context.weight / 2)).
Add(center).
Intersect(context.bounds)
}
func (context plottingContext) plotColor (center image.Point) {
square := context.square(center)
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
context.dstData[x + y * context.dstStride] = context.color
}}
}
func (context plottingContext) plotSource (center image.Point) {
square := context.square(center)
for y := square.Min.Y; y < square.Max.Y; y ++ {
for x := square.Min.X; x < square.Max.X; x ++ {
// we offset srcIndex here because we have already applied the
// offset to the square, and we need to reverse that to get the
// proper source coordinates.
srcIndex :=
x + context.offset.X +
(y + context.offset.Y) * context.dstStride
dstIndex := x + y * context.dstStride
context.dstData[dstIndex] = context.srcData [srcIndex]
}}
}

View File

@ -1,116 +0,0 @@
package shapes
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
// TODO: return updatedRegion for all routines in this package
func FillRectangle (
destination canvas.Canvas,
source canvas.Canvas,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
bounds := source.Bounds().Sub(offset).Intersect(destination.Bounds())
if bounds.Empty() { return }
updatedRegion = bounds
point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
offsetPoint := point.Add(offset)
dstIndex := point.X + point.Y * dstStride
srcIndex := offsetPoint.X + offsetPoint.Y * srcStride
dstData[dstIndex] = srcData[srcIndex]
}}
return
}
func StrokeRectangle (
destination canvas.Canvas,
source canvas.Canvas,
weight int,
) {
bounds := destination.Bounds()
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
FillRectangle(destination, source)
return
}
FillRectangleShatter(destination, source, insetBounds)
}
// FillRectangleShatter is like FillRectangle, but it does not draw in areas
// specified in "rocks".
func FillRectangleShatter (
destination canvas.Canvas,
source canvas.Canvas,
rocks ...image.Rectangle,
) {
tiles := shatter.Shatter(destination.Bounds(), rocks...)
offset := source.Bounds().Min.Sub(destination.Bounds().Min)
for _, tile := range tiles {
FillRectangle (
canvas.Cut(destination, tile),
canvas.Cut(source, tile.Add(offset)))
}
}
// FillColorRectangle fills a rectangle within the destination canvas with a
// solid color.
func FillColorRectangle (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
bounds = bounds.Canon().Intersect(destination.Bounds())
if bounds.Empty() { return }
updatedRegion = bounds
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
dstData[x + y * dstStride] = color
}}
return
}
// FillColorRectangleShatter is like FillColorRectangle, but it does not draw in
// areas specified in "rocks".
func FillColorRectangleShatter (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
rocks ...image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
for _, tile := range tiles {
FillColorRectangle(destination, color, tile)
}
}
// StrokeColorRectangle is similar to FillColorRectangle, but it draws an inset
// outline of the given rectangle instead.
func StrokeColorRectangle (
destination canvas.Canvas,
color color.RGBA,
bounds image.Rectangle,
weight int,
) {
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
FillColorRectangle(destination, color, bounds)
return
}
FillColorRectangleShatter(destination, color, bounds, insetBounds)
}

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,17 +1,13 @@
package tomo
import "errors"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "image"
// Backend represents a connection to a display server, or something similar.
// It is capable of managing an event loop, and creating windows.
type Backend interface {
// Run runs the backend's event loop. It must block until the backend
// experiences a fatal error, or Stop() is called.
Run () (err error)
Run () error
// Stop stops the backend's event loop.
Stop ()
@ -20,51 +16,36 @@ type Backend interface {
// possible. This method must be safe to call from other threads.
Do (callback func ())
// NewWindow creates a new window with the specified width and height,
// and returns a struct representing it that fulfills the Window
// interface.
NewWindow (width, height int) (window elements.Window, err error)
// NewEntity creates a new entity for the specified element.
NewEntity (owner Element) Entity
// Copy puts data into the clipboard.
Copy (data.Data)
// Paste returns the data currently in the clipboard.
Paste (accept []data.Mime) (data.Data)
// NewWindow creates a new window within the specified bounding
// rectangle. The position on screen may be overridden by the backend or
// operating system.
NewWindow (bounds image.Rectangle) (MainWindow, error)
// SetTheme sets the theme of all open windows.
SetTheme (theme.Theme)
SetTheme (Theme)
// SetConfig sets the configuration of all open windows.
SetConfig (config.Config)
SetConfig (Config)
}
// BackendFactory represents a function capable of constructing a backend
// struct. Any connections should be initialized within this function. If there
// any errors encountered during this process, the function should immediately
// stop, clean up any resources, and return an error.
type BackendFactory func () (backend Backend, err error)
var backend Backend
// RegisterBackend registers a backend factory. When an application calls
// tomo.Run(), the first registered backend that does not throw an error will be
// used.
func RegisterBackend (factory BackendFactory) {
factories = append(factories, factory)
// GetBackend returns the currently running backend.
func GetBackend () Backend {
return backend
}
var factories []BackendFactory
func instantiateBackend () (backend Backend, err error) {
// find a suitable backend
for _, factory := range factories {
backend, err = factory()
if err == nil && backend != nil { return }
}
// if none were found, but there was no error produced, produce an
// error
if err == nil {
err = errors.New("no available backends")
}
return
// SetBackend sets the currently running backend. The backend can only be set
// once—if there already is one then this function will do nothing.
func SetBackend (b Backend) {
if backend != nil { return }
backend = b
}
// Bounds creates a rectangle from an x, y, width, and height.
func Bounds (x, y, width, height int) image.Rectangle {
return image.Rect(x, y, x + width, y + height)
}

View File

@ -1,339 +0,0 @@
package x
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/xevent"
type scrollSum struct {
x, y int
}
const scrollDistance = 16
func (sum *scrollSum) add (button xproto.Button, window *Window, state uint16) {
shift :=
(state & xproto.ModMaskShift) > 0 ||
(state & window.backend.modifierMasks.shiftLock) > 0
if shift {
switch button {
case 4:
sum.x -= scrollDistance
case 5:
sum.x += scrollDistance
case 6:
sum.y -= scrollDistance
case 7:
sum.y += scrollDistance
}
} else {
switch button {
case 4:
sum.y -= scrollDistance
case 5:
sum.y += scrollDistance
case 6:
sum.x -= scrollDistance
case 7:
sum.x += scrollDistance
}
}
}
func (window *Window) handleExpose (
connection *xgbutil.XUtil,
event xevent.ExposeEvent,
) {
_ = window.compressExpose(*event.ExposeEvent)
window.redrawChildEntirely()
}
func (window *Window) handleConfigureNotify (
connection *xgbutil.XUtil,
event xevent.ConfigureNotifyEvent,
) {
if window.child == nil { return }
configureEvent := *event.ConfigureNotifyEvent
newWidth := int(configureEvent.Width)
newHeight := int(configureEvent.Height)
sizeChanged :=
window.metrics.width != newWidth ||
window.metrics.height != newHeight
window.metrics.width = newWidth
window.metrics.height = newHeight
if sizeChanged {
configureEvent = window.compressConfigureNotify(configureEvent)
window.metrics.width = int(configureEvent.Width)
window.metrics.height = int(configureEvent.Height)
window.reallocateCanvas()
window.resizeChildToFit()
if !window.exposeEventFollows(configureEvent) {
window.redrawChildEntirely()
}
}
}
func (window *Window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) {
nextEvents := xevent.Peek(window.backend.connection)
if len(nextEvents) > 0 {
untypedEvent := nextEvents[0]
if untypedEvent.Err == nil {
typedEvent, ok :=
untypedEvent.Event.(xproto.ConfigureNotifyEvent)
if ok && typedEvent.Window == event.Window {
return true
}
}
}
return false
}
func (window *Window) modifiersFromState (
state uint16,
) (
modifiers input.Modifiers,
) {
return input.Modifiers {
Shift:
(state & xproto.ModMaskShift) > 0 ||
(state & window.backend.modifierMasks.shiftLock) > 0,
Control: (state & xproto.ModMaskControl) > 0,
Alt: (state & window.backend.modifierMasks.alt) > 0,
Meta: (state & window.backend.modifierMasks.meta) > 0,
Super: (state & window.backend.modifierMasks.super) > 0,
Hyper: (state & window.backend.modifierMasks.hyper) > 0,
}
}
func (window *Window) handleKeyPress (
connection *xgbutil.XUtil,
event xevent.KeyPressEvent,
) {
if window.child == nil { return }
keyEvent := *event.KeyPressEvent
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
modifiers := window.modifiersFromState(keyEvent.State)
modifiers.NumberPad = numberPad
if key == input.KeyTab && modifiers.Alt {
if child, ok := window.child.(elements.Focusable); ok {
direction := input.KeynavDirectionForward
if modifiers.Shift {
direction = input.KeynavDirectionBackward
}
if !child.HandleFocus(direction) {
child.HandleUnfocus()
}
}
} else if child, ok := window.child.(elements.KeyboardTarget); ok {
child.HandleKeyDown(key, modifiers)
}
}
func (window *Window) handleKeyRelease (
connection *xgbutil.XUtil,
event xevent.KeyReleaseEvent,
) {
if window.child == nil { return }
keyEvent := *event.KeyReleaseEvent
// do not process this event if it was generated from a key repeat
nextEvents := xevent.Peek(window.backend.connection)
if len(nextEvents) > 0 {
untypedEvent := nextEvents[0]
if untypedEvent.Err == nil {
typedEvent, ok :=
untypedEvent.Event.(xproto.KeyPressEvent)
if ok && typedEvent.Detail == keyEvent.Detail &&
typedEvent.Event == keyEvent.Event &&
typedEvent.State == keyEvent.State {
return
}
}
}
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
modifiers := window.modifiersFromState(keyEvent.State)
modifiers.NumberPad = numberPad
if child, ok := window.child.(elements.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers)
}
}
func (window *Window) handleButtonPress (
connection *xgbutil.XUtil,
event xevent.ButtonPressEvent,
) {
if window.child == nil { return }
if child, ok := window.child.(elements.MouseTarget); ok {
buttonEvent := *event.ButtonPressEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum)
child.HandleMouseScroll (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
float64(sum.x), float64(sum.y))
} else {
child.HandleMouseDown (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
input.Button(buttonEvent.Detail))
}
}
}
func (window *Window) handleButtonRelease (
connection *xgbutil.XUtil,
event xevent.ButtonReleaseEvent,
) {
if window.child == nil { return }
if child, ok := window.child.(elements.MouseTarget); ok {
buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
child.HandleMouseUp (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
input.Button(buttonEvent.Detail))
}
}
func (window *Window) handleMotionNotify (
connection *xgbutil.XUtil,
event xevent.MotionNotifyEvent,
) {
if window.child == nil { return }
if child, ok := window.child.(elements.MouseTarget); ok {
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
child.HandleMouseMove (
int(motionEvent.EventX),
int(motionEvent.EventY))
}
}
func (window *Window) compressExpose (
firstEvent xproto.ExposeEvent,
) (
lastEvent xproto.ExposeEvent,
) {
window.backend.connection.Sync()
xevent.Read(window.backend.connection, false)
lastEvent = firstEvent
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.ExposeEvent)
if !ok { continue }
// FIXME: union all areas into the last event
if firstEvent.Window == typedEvent.Window {
lastEvent = typedEvent
defer func (index int) {
xevent.DequeueAt(window.backend.connection, index)
} (index)
}
}
return
}
func (window *Window) compressConfigureNotify (
firstEvent xproto.ConfigureNotifyEvent,
) (
lastEvent xproto.ConfigureNotifyEvent,
) {
window.backend.connection.Sync()
xevent.Read(window.backend.connection, false)
lastEvent = firstEvent
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.ConfigureNotifyEvent)
if !ok { continue }
if firstEvent.Event == typedEvent.Event &&
firstEvent.Window == typedEvent.Window {
lastEvent = typedEvent
defer func (index int) {
xevent.DequeueAt(window.backend.connection, index)
} (index)
}
}
return
}
func (window *Window) compressScrollSum (
firstEvent xproto.ButtonPressEvent,
sum *scrollSum,
) {
window.backend.connection.Sync()
xevent.Read(window.backend.connection, false)
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
if !ok { continue }
if firstEvent.Event == typedEvent.Event &&
typedEvent.Detail >= 4 &&
typedEvent.Detail <= 7 {
sum.add(typedEvent.Detail, window, typedEvent.State)
defer func (index int) {
xevent.DequeueAt(window.backend.connection, index)
} (index)
}
}
return
}
func (window *Window) compressMotionNotify (
firstEvent xproto.MotionNotifyEvent,
) (
lastEvent xproto.MotionNotifyEvent,
) {
window.backend.connection.Sync()
xevent.Read(window.backend.connection, false)
lastEvent = firstEvent
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
if untypedEvent.Err != nil { continue }
typedEvent, ok := untypedEvent.Event.(xproto.MotionNotifyEvent)
if !ok { continue }
if firstEvent.Event == typedEvent.Event {
lastEvent = typedEvent
defer func (index int) {
xevent.DequeueAt(window.backend.connection, index)
} (index)
}
}
return
}

View File

@ -1,359 +0,0 @@
package x
import "image"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/ewmh"
import "github.com/jezek/xgbutil/icccm"
import "github.com/jezek/xgbutil/xevent"
import "github.com/jezek/xgbutil/xwindow"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/elements"
type Window struct {
backend *Backend
xWindow *xwindow.Window
xCanvas *xgraphics.Image
canvas canvas.BasicCanvas
child elements.Element
onClose func ()
skipChildDrawCallback bool
theme theme.Theme
config config.Config
metrics struct {
width int
height int
}
}
func (backend *Backend) NewWindow (
width, height int,
) (
output elements.Window,
err error,
) {
if backend == nil { panic("nil backend") }
window := &Window { backend: backend }
window.xWindow, err = xwindow.Generate(backend.connection)
if err != nil { return }
window.xWindow.Create (
backend.connection.RootWin(),
0, 0, width, height, 0)
err = window.xWindow.Listen (
xproto.EventMaskExposure,
xproto.EventMaskStructureNotify,
xproto.EventMaskPointerMotion,
xproto.EventMaskKeyPress,
xproto.EventMaskKeyRelease,
xproto.EventMaskButtonPress,
xproto.EventMaskButtonRelease)
if err != nil { return }
window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) {
window.Close()
})
xevent.ExposeFun(window.handleExpose).
Connect(backend.connection, window.xWindow.Id)
xevent.ConfigureNotifyFun(window.handleConfigureNotify).
Connect(backend.connection, window.xWindow.Id)
xevent.KeyPressFun(window.handleKeyPress).
Connect(backend.connection, window.xWindow.Id)
xevent.KeyReleaseFun(window.handleKeyRelease).
Connect(backend.connection, window.xWindow.Id)
xevent.ButtonPressFun(window.handleButtonPress).
Connect(backend.connection, window.xWindow.Id)
xevent.ButtonReleaseFun(window.handleButtonRelease).
Connect(backend.connection, window.xWindow.Id)
xevent.MotionNotifyFun(window.handleMotionNotify).
Connect(backend.connection, window.xWindow.Id)
window.SetTheme(backend.theme)
window.SetConfig(backend.config)
window.metrics.width = width
window.metrics.height = height
window.childMinimumSizeChangeCallback(8, 8)
window.reallocateCanvas()
backend.windows[window.xWindow.Id] = window
output = window
return
}
func (window *Window) Adopt (child elements.Element) {
// disown previous child
if window.child != nil {
window.child.OnDamage(nil)
window.child.OnMinimumSizeChange(nil)
}
if previousChild, ok := window.child.(elements.Flexible); ok {
previousChild.OnFlexibleHeightChange(nil)
}
if previousChild, ok := window.child.(elements.Focusable); ok {
previousChild.OnFocusRequest(nil)
previousChild.OnFocusMotionRequest(nil)
if previousChild.Focused() {
previousChild.HandleUnfocus()
}
}
// adopt new child
window.child = child
if newChild, ok := child.(elements.Themeable); ok {
newChild.SetTheme(window.theme)
}
if newChild, ok := child.(elements.Configurable); ok {
newChild.SetConfig(window.config)
}
if newChild, ok := child.(elements.Flexible); ok {
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
}
if newChild, ok := child.(elements.Focusable); ok {
newChild.OnFocusRequest(window.childSelectionRequestCallback)
}
if child != nil {
child.OnDamage(window.childDrawCallback)
child.OnMinimumSizeChange (func () {
window.childMinimumSizeChangeCallback (
child.MinimumSize())
})
if !window.childMinimumSizeChangeCallback(child.MinimumSize()) {
window.resizeChildToFit()
window.redrawChildEntirely()
}
}
}
func (window *Window) Child () (child elements.Element) {
child = window.child
return
}
func (window *Window) SetTitle (title string) {
ewmh.WmNameSet (
window.backend.connection,
window.xWindow.Id,
title)
}
func (window *Window) SetIcon (sizes []image.Image) {
wmIcons := []ewmh.WmIcon { }
for _, icon := range sizes {
width := icon.Bounds().Max.X
height := icon.Bounds().Max.Y
wmIcon := ewmh.WmIcon {
Width: uint(width),
Height: uint(height),
Data: make ([]uint, width * height),
}
// manually convert image data beacuse of course we have to do
// this
index := 0
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
r, g, b, a := icon.At(x, y).RGBA()
r >>= 8
g >>= 8
b >>= 8
a >>= 8
wmIcon.Data[index] =
(uint(a) << 24) |
(uint(r) << 16) |
(uint(g) << 8) |
(uint(b) << 0)
index ++
}}
wmIcons = append(wmIcons, wmIcon)
}
ewmh.WmIconSet (
window.backend.connection,
window.xWindow.Id,
wmIcons)
}
func (window *Window) Show () {
if window.child == nil {
window.xCanvas.For (func (x, y int) xgraphics.BGRA {
return xgraphics.BGRA { }
})
window.pushRegion(window.xCanvas.Bounds())
}
window.xWindow.Map()
}
func (window *Window) Hide () {
window.xWindow.Unmap()
}
func (window *Window) Close () {
if window.onClose != nil { window.onClose() }
delete(window.backend.windows, window.xWindow.Id)
window.xWindow.Destroy()
}
func (window *Window) OnClose (callback func ()) {
window.onClose = callback
}
func (window *Window) SetTheme (theme theme.Theme) {
window.theme = theme
if child, ok := window.child.(elements.Themeable); ok {
child.SetTheme(theme)
}
}
func (window *Window) SetConfig (config config.Config) {
window.config = config
if child, ok := window.child.(elements.Configurable); ok {
child.SetConfig(config)
}
}
func (window *Window) reallocateCanvas () {
window.canvas.Reallocate(window.metrics.width, window.metrics.height)
previousWidth, previousHeight := 0, 0
if window.xCanvas != nil {
previousWidth = window.xCanvas.Bounds().Dx()
previousHeight = window.xCanvas.Bounds().Dy()
}
newWidth := window.metrics.width
newHeight := window.metrics.height
larger := newWidth > previousWidth || newHeight > previousHeight
smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2
if larger || smaller {
if window.xCanvas != nil {
window.xCanvas.Destroy()
}
window.xCanvas = xgraphics.New (
window.backend.connection,
image.Rect (
0, 0,
(newWidth / 64) * 64 + 64,
(newHeight / 64) * 64 + 64))
window.xCanvas.CreatePixmap()
}
}
func (window *Window) redrawChildEntirely () {
window.pushRegion(window.paste(window.canvas))
}
func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true
if child, ok := window.child.(elements.Flexible); ok {
minimumHeight := child.FlexibleHeightFor(window.metrics.width)
minimumWidth, _ := child.MinimumSize()
icccm.WmNormalHintsSet (
window.backend.connection,
window.xWindow.Id,
&icccm.NormalHints {
Flags: icccm.SizeHintPMinSize,
MinWidth: uint(minimumWidth),
MinHeight: uint(minimumHeight),
})
if window.metrics.height >= minimumHeight &&
window.metrics.width >= minimumWidth {
window.child.DrawTo(window.canvas)
}
} else {
window.child.DrawTo(window.canvas)
}
window.skipChildDrawCallback = false
}
func (window *Window) childDrawCallback (region canvas.Canvas) {
if window.skipChildDrawCallback { return }
window.pushRegion(window.paste(region))
}
func (window *Window) paste (canvas canvas.Canvas) (updatedRegion image.Rectangle) {
data, stride := canvas.Buffer()
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
rgba := data[x + y * stride]
index := x * 4 + y * window.xCanvas.Stride
window.xCanvas.Pix[index + 0] = rgba.B
window.xCanvas.Pix[index + 1] = rgba.G
window.xCanvas.Pix[index + 2] = rgba.R
window.xCanvas.Pix[index + 3] = rgba.A
}}
return bounds
}
func (window *Window) childMinimumSizeChangeCallback (width, height int) (resized bool) {
icccm.WmNormalHintsSet (
window.backend.connection,
window.xWindow.Id,
&icccm.NormalHints {
Flags: icccm.SizeHintPMinSize,
MinWidth: uint(width),
MinHeight: uint(height),
})
newWidth := window.metrics.width
newHeight := window.metrics.height
if newWidth < width { newWidth = width }
if newHeight < height { newHeight = height }
if newWidth != window.metrics.width ||
newHeight != window.metrics.height {
window.xWindow.Resize(newWidth, newHeight)
return true
}
return false
}
func (window *Window) childSelectionRequestCallback () (granted bool) {
if _, ok := window.child.(elements.Focusable); ok {
return true
}
return false
}
func (window *Window) childSelectionMotionRequestCallback (
direction input.KeynavDirection,
) (
granted bool,
) {
if child, ok := window.child.(elements.Focusable); ok {
if !child.HandleFocus(direction) {
child.HandleUnfocus()
}
return true
}
return true
}
func (window *Window) pushRegion (region image.Rectangle) {
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
if ok {
image.XDraw()
image.XExpPaint (
window.xWindow.Id,
image.Bounds().Min.X,
image.Bounds().Min.Y)
}
}

View File

@ -1,140 +0,0 @@
package x
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "github.com/jezek/xgbutil"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/xevent"
// Backend is an instance of an X backend.
type Backend struct {
connection *xgbutil.XUtil
doChannel chan(func ())
modifierMasks struct {
capsLock uint16
shiftLock uint16
numLock uint16
modeSwitch uint16
alt uint16
meta uint16
super uint16
hyper uint16
}
theme theme.Theme
config config.Config
windows map[xproto.Window] *Window
open bool
}
// NewBackend instantiates an X backend.
func NewBackend () (output tomo.Backend, err error) {
backend := &Backend {
windows: map[xproto.Window] *Window { },
doChannel: make(chan func (), 0),
theme: theme.Default { },
config: config.Default { },
open: true,
}
// connect to X
backend.connection, err = xgbutil.NewConn()
if err != nil { return }
backend.initializeKeymapInformation()
output = backend
return
}
// Run runs the backend's event loop. This method will not exit until Stop() is
// called, or the backend experiences a fatal error.
func (backend *Backend) Run () (err error) {
backend.assert()
pingBefore,
pingAfter,
pingQuit := xevent.MainPing(backend.connection)
for {
select {
case <- pingBefore:
<- pingAfter
case callback := <- backend.doChannel:
callback()
case <- pingQuit:
return
}
}
}
// Stop gracefully closes the connection and stops the event loop.
func (backend *Backend) Stop () {
backend.assert()
if !backend.open { return }
backend.open = false
toClose := []*Window { }
for _, window := range backend.windows {
toClose = append(toClose, window)
}
for _, window := range toClose {
window.Close()
}
xevent.Quit(backend.connection)
backend.connection.Conn().Close()
}
// Do executes the specified callback within the main thread as soon as
// possible. This function can be safely called from other threads.
func (backend *Backend) Do (callback func ()) {
backend.assert()
backend.doChannel <- callback
}
// Copy puts data into the clipboard. This method is not yet implemented and
// will do nothing!
func (backend *Backend) Copy (data data.Data) {
backend.assert()
// TODO
}
// Paste returns the data currently in the clipboard. This method may
// return nil. This method is not yet implemented and will do nothing!
func (backend *Backend) Paste (accept []data.Mime) (data data.Data) {
backend.assert()
// TODO
return
}
// SetTheme sets the theme of all open windows.
func (backend *Backend) SetTheme (theme theme.Theme) {
backend.assert()
backend.theme = theme
for _, window := range backend.windows {
window.SetTheme(theme)
}
}
// SetConfig sets the configuration of all open windows.
func (backend *Backend) SetConfig (config config.Config) {
backend.assert()
backend.config = config
for _, window := range backend.windows {
window.SetConfig(config)
}
}
func (backend *Backend) assert () {
if backend == nil { panic("nil backend") }
}
func init () {
tomo.RegisterBackend(NewBackend)
}

View File

@ -1,111 +0,0 @@
package canvas
import "image"
import "image/draw"
import "image/color"
// Image represents an immutable canvas.
type Image interface {
image.Image
RGBAAt (x, y int) color.RGBA
}
// Canvas is like draw.Image but is also able to return a raw pixel buffer for
// more efficient drawing. This interface can be easily satisfied using a
// BasicCanvas struct.
type Canvas interface {
draw.Image
Buffer () (data []color.RGBA, stride int)
}
// BasicCanvas is a general purpose implementation of tomo.Canvas.
type BasicCanvas struct {
pix []color.RGBA
stride int
rect image.Rectangle
}
// NewBasicCanvas creates a new basic canvas with the specified width and
// height, allocating a buffer for it.
func NewBasicCanvas (width, height int) (canvas BasicCanvas) {
canvas.pix = make([]color.RGBA, height * width)
canvas.stride = width
canvas.rect = image.Rect(0, 0, width, height)
return
}
// FromImage creates a new BasicCanvas from an image.Image.
func FromImage (img image.Image) (canvas BasicCanvas) {
bounds := img.Bounds()
canvas = NewBasicCanvas(bounds.Dx(), bounds.Dy())
point := image.Point { }
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
canvasPoint := point.Sub(bounds.Min)
canvas.Set (
canvasPoint.X, canvasPoint.Y,
img.At(point.X, point.Y))
}}
return
}
// you know what it do
func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) {
return canvas.rect
}
// you know what it do
func (canvas BasicCanvas) At (x, y int) (color.Color) {
if !image.Pt(x, y).In(canvas.rect) { return nil }
return canvas.pix[x + y * canvas.stride]
}
// you know what it do
func (canvas BasicCanvas) ColorModel () (model color.Model) {
return color.RGBAModel
}
// you know what it do
func (canvas BasicCanvas) Set (x, y int, c color.Color) {
if !image.Pt(x, y).In(canvas.rect) { return }
r, g, b, a := c.RGBA()
canvas.pix[x + y * canvas.stride] = color.RGBA {
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8(a >> 8),
}
}
// you know what it do
func (canvas BasicCanvas) Buffer () (data []color.RGBA, stride int) {
return canvas.pix, canvas.stride
}
// Reallocate efficiently reallocates the canvas. The data within will be
// garbage. This method will do nothing if this is a cut image.
func (canvas *BasicCanvas) Reallocate (width, height int) {
if canvas.rect.Min != (image.Point { }) { return }
previousLen := len(canvas.pix)
newLen := width * height
bigger := newLen > previousLen
smaller := newLen < previousLen / 2
if bigger || smaller {
canvas.pix = make (
[]color.RGBA,
((height * width) / 4096) * 4096 + 4096)
}
canvas.stride = width
canvas.rect = image.Rect(0, 0, width, height)
}
// Cut returns a sub-canvas of a given canvas.
func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) {
// println(canvas.Bounds().String(), bounds.String())
bounds = bounds.Intersect(canvas.Bounds())
if bounds.Empty() { return }
reduced.rect = bounds
reduced.pix, reduced.stride = canvas.Buffer()
return
}

View File

@ -1,4 +0,0 @@
// Package canvas provides a canvas interface that is able to return a pixel
// buffer for drawing. This makes it considerably more efficient than the
// standard draw.Image.
package canvas

14
config.go Normal file
View File

@ -0,0 +1,14 @@
package tomo
import "time"
// Config can return global configuration parameters.
type Config interface {
// ScrollVelocity returns how many pixels should be scrolled every time
// a scroll button is pressed.
ScrollVelocity () int
// DoubleClickDelay returns the maximum delay between two clicks for
// them to be registered as a double click.
DoubleClickDelay () time.Duration
}

View File

@ -1,62 +0,0 @@
package config
// Config can return global configuration parameters.
type Config interface {
// HandleWidth returns how large grab handles should typically be. This
// is important for accessibility reasons.
HandleWidth () int
// ScrollVelocity returns how many pixels should be scrolled every time
// a scroll button is pressed.
ScrollVelocity () int
// ThemePath returns the directory path to the theme.
ThemePath () string
}
// Default specifies default configuration values.
type Default struct { }
// HandleWidth returns the default handle width value.
func (Default) HandleWidth () int {
return 16
}
// ScrollVelocity returns the default scroll velocity value.
func (Default) ScrollVelocity () int {
return 16
}
// ThemePath returns the default theme path.
func (Default) ThemePath () (string) {
return ""
}
// Wrapped wraps a configuration and uses Default if it is nil.
type Wrapped struct {
Config
}
// HandleWidth returns how large grab handles should typically be. This
// is important for accessibility reasons.
func (wrapped Wrapped) HandleWidth () int {
return wrapped.ensure().HandleWidth()
}
// ScrollVelocity returns how many pixels should be scrolled every time
// a scroll button is pressed.
func (wrapped Wrapped) ScrollVelocity () int {
return wrapped.ensure().ScrollVelocity()
}
// ThemePath returns the directory path to the theme.
func (wrapped Wrapped) ThemePath () string {
return wrapped.ensure().ThemePath()
}
func (wrapped Wrapped) ensure () (real Config) {
real = wrapped.Config
if real == nil { real = Default { } }
return
}

View File

@ -1,9 +0,0 @@
package config
import "io"
// Parse parses one or more configuration files and returns them as a Config.
func Parse (sources ...io.Reader) (config Config) {
// TODO
return Default { }
}

View File

@ -1,10 +1,12 @@
// Package data provides operations to deal with arbitrary data and MIME types.
package data
import "io"
import "bytes"
// Data represents arbitrary polymorphic data that can be used for data transfer
// between applications.
type Data map[Mime] io.ReadCloser
type Data map[Mime] io.ReadSeekCloser
// Mime represents a MIME type.
type Mime struct {
@ -15,6 +17,41 @@ type Mime struct {
Type, Subtype string
}
// M is shorthand for creating a MIME type.
func M (ty, subtype string) Mime {
return Mime { ty, subtype }
}
// String returns the string representation of the MIME type.
func (mime Mime) String () string {
return mime.Type + "/" + mime.Subtype
}
var MimePlain = Mime { "text", "plain" }
var MimeFile = Mime { "text", "uri-list" }
type byteReadCloser struct { *bytes.Reader }
func (byteReadCloser) Close () error { return nil }
// Text returns plain text Data given a string.
func Text (text string) Data {
return Bytes(MimePlain, []byte(text))
}
// Bytes constructs a Data given a buffer and a mime type.
func Bytes (mime Mime, buffer []byte) Data {
return Data {
mime: byteReadCloser { bytes.NewReader(buffer) },
}
}
// Merge combines several Datas together. If multiple Datas provide a reader for
// the same mime type, the ones further on in the list will take precedence.
func Merge (individual ...Data) (combined Data) {
for _, data := range individual {
for mime, reader := range data {
combined[mime] = reader
}}
return
}

41
default/config/config.go Normal file
View File

@ -0,0 +1,41 @@
package config
import "time"
import "tomo"
// Default specifies default configuration values.
type Default struct { }
// ScrollVelocity returns the default scroll velocity value.
func (Default) ScrollVelocity () int {
return 16
}
// DoubleClickDelay returns the default double click delay.
func (Default) DoubleClickDelay () time.Duration {
return time.Second / 2
}
// Wrapped wraps a configuration and uses Default if it is nil.
type Wrapped struct {
tomo.Config
}
// ScrollVelocity returns how many pixels should be scrolled every time a scroll
// button is pressed.
func (wrapped Wrapped) ScrollVelocity () int {
return wrapped.ensure().ScrollVelocity()
}
// DoubleClickDelay returns the maximum delay between two clicks for them to be
// registered as a double click.
func (wrapped Wrapped) DoubleClickDelay () time.Duration {
return wrapped.ensure().DoubleClickDelay()
}
func (wrapped Wrapped) ensure () (real tomo.Config) {
real = wrapped.Config
if real == nil { real = Default { } }
return
}

2
default/config/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package config implements a default configuration.
package config

9
default/config/parse.go Normal file
View File

@ -0,0 +1,9 @@
package config
// import "io"
// Parse parses one or more configuration files and returns them as a Config.
// func Parse (sources ...io.Reader) (config tomo.Config) {
// // TODO
// return Default { }
// }

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

250
default/theme/default.go Normal file
View File

@ -0,0 +1,250 @@
package theme
import "image"
import "bytes"
import _ "embed"
import _ "image/png"
import "image/color"
import "golang.org/x/image/font"
import "golang.org/x/image/font/basicfont"
import "tomo"
import "tomo/data"
import "art"
import "art/artutil"
import "art/patterns"
//go:embed assets/default.png
var defaultAtlasBytes []byte
var defaultAtlas art.Canvas
var defaultTextures [7][7]art.Pattern
//go:embed assets/wintergreen-icons-small.png
var defaultIconsSmallAtlasBytes []byte
var defaultIconsSmall [640]binaryIcon
//go:embed assets/wintergreen-icons-large.png
var defaultIconsLargeAtlasBytes []byte
var defaultIconsLarge [640]binaryIcon
func atlasCell (col, row int, border art.Inset) {
bounds := image.Rect(0, 0, 8, 8).Add(image.Pt(col, row).Mul(8))
defaultTextures[col][row] = patterns.Border {
Canvas: art.Cut(defaultAtlas, bounds),
Inset: border,
}
}
func atlasCol (col int, border art.Inset) {
for index, _ := range defaultTextures[col] {
atlasCell(col, index, border)
}
}
type binaryIcon struct {
data []bool
stride int
}
func (icon binaryIcon) Draw (destination art.Canvas, color color.RGBA, at image.Point) {
bounds := icon.Bounds().Add(at).Intersect(destination.Bounds())
point := image.Point { }
data, stride := destination.Buffer()
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
srcPoint := point.Sub(at)
srcIndex := srcPoint.X + srcPoint.Y * icon.stride
dstIndex := point.X + point.Y * stride
if icon.data[srcIndex] {
data[dstIndex] = color
}
}}
}
func (icon binaryIcon) Bounds () image.Rectangle {
return image.Rect(0, 0, icon.stride, len(icon.data) / icon.stride)
}
func binaryIconFrom (source image.Image, clip image.Rectangle) (icon binaryIcon) {
bounds := source.Bounds().Intersect(clip)
if bounds.Empty() { return }
icon.stride = bounds.Dx()
icon.data = make([]bool, bounds.Dx() * bounds.Dy())
point := image.Point { }
dstIndex := 0
for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ {
for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ {
r, g, b, a := source.At(point.X, point.Y).RGBA()
if a > 0x8000 && (r + g + b) / 3 < 0x8000 {
icon.data[dstIndex] = true
}
dstIndex ++
}}
return
}
func init () {
defaultAtlasImage, _, _ := image.Decode(bytes.NewReader(defaultAtlasBytes))
defaultAtlas = art.FromImage(defaultAtlasImage)
atlasCol(0, art.I(0))
atlasCol(1, art.I(3))
atlasCol(2, art.I(1))
atlasCol(3, art.I(1))
atlasCol(4, art.I(1))
atlasCol(5, art.I(3))
atlasCol(6, art.I(1))
// set up small icons
defaultIconsSmallAtlasImage, _, _ := image.Decode (
bytes.NewReader(defaultIconsSmallAtlasBytes))
point := image.Point { }
iconIndex := 0
for point.Y = 0; point.Y < 20; point.Y ++ {
for point.X = 0; point.X < 32; point.X ++ {
defaultIconsSmall[iconIndex] = binaryIconFrom (
defaultIconsSmallAtlasImage,
image.Rect(0, 0, 16, 16).Add(point.Mul(16)))
iconIndex ++
}}
// set up large icons
defaultIconsLargeAtlasImage, _, _ := image.Decode (
bytes.NewReader(defaultIconsLargeAtlasBytes))
point = image.Point { }
iconIndex = 0
for point.Y = 0; point.Y < 8; point.Y ++ {
for point.X = 0; point.X < 32; point.X ++ {
defaultIconsLarge[iconIndex] = binaryIconFrom (
defaultIconsLargeAtlasImage,
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
iconIndex ++
}}
iconIndex = 384
for point.Y = 8; point.Y < 12; point.Y ++ {
for point.X = 0; point.X < 32; point.X ++ {
defaultIconsLarge[iconIndex] = binaryIconFrom (
defaultIconsLargeAtlasImage,
image.Rect(0, 0, 32, 32).Add(point.Mul(32)))
iconIndex ++
}}
}
// Default is the default theme.
type Default struct { }
// FontFace returns the default font face.
func (Default) FontFace (style tomo.FontStyle, size tomo.FontSize, c tomo.Case) font.Face {
return basicfont.Face7x13
}
// Icon returns an icon from the default set corresponding to the given name.
func (Default) Icon (id tomo.Icon, size tomo.IconSize, c tomo.Case) art.Icon {
if size == tomo.IconSizeLarge {
if id < 0 || int(id) >= len(defaultIconsLarge) {
return nil
} else {
return defaultIconsLarge[id]
}
} else {
if id < 0 || int(id) >= len(defaultIconsSmall) {
return nil
} else {
return defaultIconsSmall[id]
}
}
}
// MimeIcon returns an icon from the default set corresponding to the given mime.
// type.
func (Default) MimeIcon (data.Mime, tomo.IconSize, tomo.Case) art.Icon {
// TODO
return nil
}
// Pattern returns a pattern from the default theme corresponding to the given
// pattern ID.
func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) art.Pattern {
offset := 0; switch {
case state.Disabled: offset = 1
case state.Pressed && state.On: offset = 4
case state.Focused && state.On: offset = 6
case state.On: offset = 2
case state.Pressed: offset = 3
case state.Focused: offset = 5
}
switch id {
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
case tomo.PatternDead: return defaultTextures[0][offset]
case tomo.PatternRaised: return defaultTextures[1][offset]
case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternButton: return defaultTextures[1][offset]
case tomo.PatternInput: return defaultTextures[2][offset]
case tomo.PatternGutter: return defaultTextures[2][offset]
case tomo.PatternHandle: return defaultTextures[3][offset]
case tomo.PatternLine: return defaultTextures[0][offset]
case tomo.PatternMercury: return defaultTextures[4][offset]
case tomo.PatternTableHead: return defaultTextures[5][offset]
case tomo.PatternTableCell: return defaultTextures[5][offset]
case tomo.PatternLamp: return defaultTextures[6][offset]
default: return patterns.Uhex(0xFF00FFFF)
}
}
func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
if state.Disabled { return artutil.Hex(0x444444FF) }
return artutil.Hex (map[tomo.Color] uint32 {
tomo.ColorBlack: 0x272d24FF,
tomo.ColorRed: 0x8c4230FF,
tomo.ColorGreen: 0x69905fFF,
tomo.ColorYellow: 0x9a973dFF,
tomo.ColorBlue: 0x3d808fFF,
tomo.ColorPurple: 0x8c608bFF,
tomo.ColorCyan: 0x3d8f84FF,
tomo.ColorWhite: 0xaea894FF,
tomo.ColorBrightBlack: 0x4f5142FF,
tomo.ColorBrightRed: 0xbd6f59FF,
tomo.ColorBrightGreen: 0x8dad84FF,
tomo.ColorBrightYellow: 0xe2c558FF,
tomo.ColorBrightBlue: 0x77b1beFF,
tomo.ColorBrightPurple: 0xc991c8FF,
tomo.ColorBrightCyan: 0x74c7b7FF,
tomo.ColorBrightWhite: 0xcfd7d2FF,
tomo.ColorForeground: 0x000000FF,
tomo.ColorMidground: 0x656565FF,
tomo.ColorBackground: 0xAAAAAAFF,
tomo.ColorShadow: 0x000000FF,
tomo.ColorShine: 0xFFFFFFFF,
tomo.ColorAccent: 0xff3300FF,
} [id])
}
// Padding returns the default padding value for the given pattern.
func (Default) Padding (id tomo.Pattern, c tomo.Case) art.Inset {
switch id {
case tomo.PatternGutter: return art.I(0)
case tomo.PatternLine: return art.I(1)
default: return art.I(6)
}
}
// Margin returns the default margin value for the given pattern.
func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point {
return image.Pt(6, 6)
}
// Hints returns rendering optimization hints for a particular pattern.
// These are optional, but following them may result in improved
// performance.
func (Default) Hints (pattern tomo.Pattern, c tomo.Case) (hints tomo.Hints) {
return
}
// Sink returns the default sink vector for the given pattern.
func (Default) Sink (pattern tomo.Pattern, c tomo.Case) image.Point {
return image.Point { 1, 1 }
}

2
default/theme/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package theme implements a default theme.
package theme

View File

@ -1,47 +0,0 @@
package defaultfont
import "golang.org/x/image/font/basicfont"
var FaceRegular = basicfont.Face7x13
// FIXME: make bold, italic, and bold italic masks by processing the Face7x13
// mask.
var FaceBold = &basicfont.Face {
Advance: 7,
Width: 6,
Height: 13,
Ascent: 11,
Descent: 2,
Mask: FaceRegular.Mask, // TODO
Ranges: []basicfont.Range {
{ '\u0020', '\u007f', 0 },
{ '\ufffd', '\ufffe', 95 },
},
}
var FaceItalic = &basicfont.Face {
Advance: 7,
Width: 6,
Height: 13,
Ascent: 11,
Descent: 2,
Mask: FaceRegular.Mask, // TODO
Ranges: []basicfont.Range {
{ '\u0020', '\u007f', 0 },
{ '\ufffd', '\ufffe', 95 },
},
}
var FaceBoldItalic = &basicfont.Face {
Advance: 7,
Width: 6,
Height: 13,
Ascent: 11,
Descent: 2,
Mask: FaceRegular.Mask, // TODO
Ranges: []basicfont.Range {
{ '\u0020', '\u007f', 0 },
{ '\ufffd', '\ufffe', 95 },
},
}

View File

@ -1,3 +1,4 @@
// Package dirs provides access to standard system and user directories.
package dirs
import "os"

15
element.go Normal file
View File

@ -0,0 +1,15 @@
package tomo
import "art"
// Element represents a basic on-screen object. Extended element interfaces are
// defined in the ability module.
type Element interface {
// Draw causes the element to draw to the specified canvas. The bounds
// of this canvas specify the area that is actually drawn to, while the
// Entity bounds specify the actual area of the element.
Draw (art.Canvas)
// Entity returns this element's entity.
Entity () Entity
}

View File

@ -1,198 +0,0 @@
package basicElements
import "image"
// import "runtime/debug"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Button is a clickable button.
type Button struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
drawer textdraw.Drawer
pressed bool
text string
config config.Wrapped
theme theme.Wrapped
onClick func ()
}
// NewButton creates a new button with the specified label text.
func NewButton (text string) (element *Button) {
element = &Button { }
element.theme.Case = theme.C("basic", "button")
element.Core, element.core = core.NewCore(element.drawAll)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
element.drawAndPush(true)
})
element.SetText(text)
return
}
func (element *Button) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if button != input.ButtonLeft { return }
element.pressed = true
element.drawAndPush(true)
}
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
element.pressed = false
within := image.Point { x, y }.
In(element.Bounds())
if element.Enabled() && within && element.onClick != nil {
element.onClick()
}
element.drawAndPush(true)
}
func (element *Button) HandleMouseMove (x, y int) { }
func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
if key == input.KeyEnter {
element.pressed = true
element.drawAndPush(true)
}
}
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.drawAndPush(true)
if !element.Enabled() { return }
if element.onClick != nil {
element.onClick()
}
}
}
// OnClick sets the function to be called when the button is clicked.
func (element *Button) OnClick (callback func ()) {
element.onClick = callback
}
// SetEnabled sets whether this button can be clicked or not.
func (element *Button) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the button's label text.
func (element *Button) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.drawAndPush(false)
}
// SetTheme sets the element's theme.
func (element *Button) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal))
element.updateMinimumSize()
element.drawAndPush(false)
}
// SetConfig sets the element's configuration.
func (element *Button) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.drawAndPush(false)
}
func (element *Button) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
padding := element.theme.Padding(theme.PatternButton)
minimumSize := padding.Inverse().Apply(textBounds)
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
}
func (element *Button) drawAndPush (partial bool) {
if element.core.HasImage () {
if partial {
element.core.DamageRegion (append (
element.drawBackground(true),
element.drawText(true))...)
} else {
element.drawAll()
element.core.DamageAll()
}
}
}
func (element *Button) state () theme.State {
return theme.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
}
}
func (element *Button) drawBackground (partial bool) []image.Rectangle {
state := element.state()
bounds := element.Bounds()
pattern := element.theme.Pattern(theme.PatternButton, state)
static := element.theme.Hints(theme.PatternButton).StaticInset
if partial && static != (artist.Inset { }) {
tiles := shatter.Shatter(bounds, static.Apply(bounds))
artist.Draw(element.core, pattern, tiles...)
return tiles
} else {
pattern.Draw(element.core, bounds)
return []image.Rectangle { bounds }
}
}
func (element *Button) drawText (partial bool) image.Rectangle {
state := element.state()
bounds := element.Bounds()
foreground := element.theme.Color(theme.ColorForeground, state)
sink := element.theme.Sink(theme.PatternButton)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
X: bounds.Min.X + (bounds.Dx() - textBounds.Dx()) / 2,
Y: bounds.Min.Y + (bounds.Dy() - textBounds.Dy()) / 2,
}
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
region := textBounds.Union(textBounds.Add(sink)).Add(offset)
if element.pressed {
offset = offset.Add(sink)
}
if partial {
pattern := element.theme.Pattern(theme.PatternButton, state)
pattern.Draw(element.core, region)
}
element.drawer.Draw(element.core, foreground, offset)
return region
}
func (element *Button) drawAll () {
element.drawBackground(false)
element.drawText(false)
}

View File

@ -1,192 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Checkbox is a toggle-able checkbox with a label.
type Checkbox struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
drawer textdraw.Drawer
pressed bool
checked bool
text string
config config.Wrapped
theme theme.Wrapped
onToggle func ()
}
// NewCheckbox creates a new cbeckbox with the specified label text.
func NewCheckbox (text string, checked bool) (element *Checkbox) {
element = &Checkbox { checked: checked }
element.theme.Case = theme.C("basic", "checkbox")
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.redo)
element.SetText(text)
return
}
func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false
within := image.Point { x, y }.
In(element.Bounds())
if within {
element.checked = !element.checked
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Checkbox) HandleMouseMove (x, y int) { }
func (element *Checkbox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onToggle != nil {
element.onToggle()
}
}
}
// OnToggle sets the function to be called when the checkbox is toggled.
func (element *Checkbox) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the checkbox is currently checked.
func (element *Checkbox) Value () (checked bool) {
return element.checked
}
// SetEnabled sets whether this checkbox can be toggled or not.
func (element *Checkbox) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the checkbox's label text.
func (element *Checkbox) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
// SetTheme sets the element's theme.
func (element *Checkbox) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal))
element.updateMinimumSize()
element.redo()
}
// SetConfig sets the element's configuration.
func (element *Checkbox) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
}
func (element *Checkbox) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
if element.text == "" {
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
} else {
margin := element.theme.Margin(theme.PatternBackground)
element.core.SetMinimumSize (
textBounds.Dy() + margin.X + textBounds.Dx(),
textBounds.Dy())
}
}
func (element *Checkbox) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Checkbox) draw () {
bounds := element.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
state := theme.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
On: element.checked,
}
backgroundPattern := element.theme.Pattern (
theme.PatternBackground, state)
backgroundPattern.Draw(element.core, bounds)
pattern := element.theme.Pattern(theme.PatternButton, state)
artist.DrawBounds(element.core, pattern, boxBounds)
textBounds := element.drawer.LayoutBounds()
margin := element.theme.Margin(theme.PatternBackground)
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + margin.X,
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.theme.Color(theme.ColorForeground, state)
element.drawer.Draw(element.core, foreground, offset)
}

View File

@ -1,536 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Container is an element capable of containg other elements, and arranging
// them in a layout.
type Container struct {
*core.Core
core core.CoreControl
layout layouts.Layout
children []layouts.LayoutEntry
drags [10]elements.MouseTarget
warping bool
focused bool
focusable bool
flexible bool
config config.Wrapped
theme theme.Wrapped
onFocusRequest func () (granted bool)
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
onFlexibleHeightChange func ()
}
// NewContainer creates a new container.
func NewContainer (layout layouts.Layout) (element *Container) {
element = &Container { }
element.theme.Case = theme.C("basic", "container")
element.Core, element.core = core.NewCore(element.redoAll)
element.SetLayout(layout)
return
}
// SetLayout sets the layout of this container.
func (element *Container) SetLayout (layout layouts.Layout) {
element.layout = layout
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
}
// Adopt adds a new child element to the container. If expand is set to true,
// the element will expand (instead of contract to its minimum size), in
// whatever way is defined by the current layout.
func (element *Container) Adopt (child elements.Element, expand bool) {
// set event handlers
if child0, ok := child.(elements.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
if child0, ok := child.(elements.Configurable); ok {
child0.SetConfig(element.config.Config)
}
child.OnDamage (func (region canvas.Canvas) {
element.core.DamageRegion(region.Bounds())
})
child.OnMinimumSizeChange (func () {
// TODO: this could probably stand to be more efficient. I mean
// seriously?
element.updateMinimumSize()
element.redoAll()
element.core.DamageAll()
})
if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(element.updateMinimumSize)
}
if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest (func () (granted bool) {
return element.childFocusRequestCallback(child0)
})
child0.OnFocusMotionRequest (
func (direction input.KeynavDirection) (granted bool) {
if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction)
})
}
// add child
element.children = append (element.children, layouts.LayoutEntry {
Element: child,
Expand: expand,
})
// refresh stale data
element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// Warp runs the specified callback, deferring all layout and rendering updates
// until the callback has finished executing. This allows for aplications to
// perform batch gui updates without flickering and stuff.
func (element *Container) Warp (callback func ()) {
if element.warping {
callback()
return
}
element.warping = true
callback()
element.warping = false
// TODO: create some sort of task list so we don't do a full recalculate
// and redraw every time, because although that is the most likely use
// case, it is not the only one.
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
}
// Disown removes the given child from the container if it is contained within
// it.
func (element *Container) Disown (child elements.Element) {
for index, entry := range element.children {
if entry.Element == child {
element.clearChildEventHandlers(entry.Element)
element.children = append (
element.children[:index],
element.children[index + 1:]...)
break
}
}
element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
func (element *Container) clearChildEventHandlers (child elements.Element) {
child.DrawTo(nil)
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil)
if child0.Focused() {
child0.HandleUnfocus()
}
}
if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(nil)
}
}
// DisownAll removes all child elements from the container at once.
func (element *Container) DisownAll () {
element.children = nil
element.updateMinimumSize()
element.reflectChildProperties()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// Children returns a slice containing this element's children.
func (element *Container) Children () (children []elements.Element) {
children = make([]elements.Element, len(element.children))
for index, entry := range element.children {
children[index] = entry.Element
}
return
}
// CountChildren returns the amount of children contained within this element.
func (element *Container) CountChildren () (count int) {
return len(element.children)
}
// Child returns the child at the specified index. If the index is out of
// bounds, this method will return nil.
func (element *Container) Child (index int) (child elements.Element) {
if index < 0 || index > len(element.children) { return }
return element.children[index].Element
}
// ChildAt returns the child that contains the specified x and y coordinates. If
// there are no children at the coordinates, this method will return nil.
func (element *Container) ChildAt (point image.Point) (child elements.Element) {
for _, entry := range element.children {
if point.In(entry.Bounds) {
child = entry.Element
}
}
return
}
func (element *Container) childPosition (child elements.Element) (position image.Point) {
for _, entry := range element.children {
if entry.Element == child {
position = entry.Bounds.Min
break
}
}
return
}
func (element *Container) redoAll () {
if !element.core.HasImage() { return }
// do a layout
element.doLayout()
// draw a background
rocks := make([]image.Rectangle, len(element.children))
for index, entry := range element.children {
rocks[index] = entry.Bounds
}
pattern := element.theme.Pattern (
theme.PatternBackground,
theme.State { })
artist.DrawShatter (
element.core, pattern, rocks...)
// cut our canvas up and give peices to child elements
for _, entry := range element.children {
entry.DrawTo(canvas.Cut(element.core, entry.Bounds))
}
}
// SetTheme sets the element's theme.
func (element *Container) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
for _, child := range element.children {
if child0, ok := child.Element.(elements.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
}
element.updateMinimumSize()
element.redoAll()
}
// SetConfig sets the element's configuration.
func (element *Container) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
for _, child := range element.children {
if child0, ok := child.Element.(elements.Configurable); ok {
child0.SetConfig(element.config)
}
}
element.updateMinimumSize()
element.redoAll()
}
func (element *Container) HandleMouseDown (x, y int, button input.Button) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
if !handlesMouse { return }
element.drags[button] = child
child.HandleMouseDown(x, y, button)
}
func (element *Container) HandleMouseUp (x, y int, button input.Button) {
child := element.drags[button]
if child == nil { return }
element.drags[button] = nil
child.HandleMouseUp(x, y, button)
}
func (element *Container) HandleMouseMove (x, y int) {
for _, child := range element.drags {
if child == nil { continue }
child.HandleMouseMove(x, y)
}
}
func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
if !handlesMouse { return }
child.HandleMouseScroll(x, y, deltaX, deltaY)
}
func (element *Container) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
element.forFocused (func (child elements.Focusable) bool {
child0, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyDown(key, modifiers)
}
return true
})
}
func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
element.forFocused (func (child elements.Focusable) bool {
child0, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyUp(key, modifiers)
}
return true
})
}
func (element *Container) FlexibleHeightFor (width int) (height int) {
margin := element.theme.Margin(theme.PatternBackground)
// TODO: have layouts take in x and y margins
return element.layout.FlexibleHeightFor (
element.children,
margin.X, width)
}
func (element *Container) OnFlexibleHeightChange (callback func ()) {
element.onFlexibleHeightChange = callback
}
func (element *Container) Focused () (focused bool) {
return element.focused
}
func (element *Container) Focus () {
if element.onFocusRequest != nil {
element.onFocusRequest()
}
}
func (element *Container) HandleFocus (direction input.KeynavDirection) (ok bool) {
if !element.focusable { return false }
direction = direction.Canon()
firstFocused := element.firstFocused()
if firstFocused < 0 {
// no element is currently focused, so we need to focus either
// the first or last focusable element depending on the
// direction.
switch direction {
case input.KeynavDirectionNeutral, input.KeynavDirectionForward:
// if we recieve a neutral or forward direction, focus
// the first focusable element.
return element.focusFirstFocusableElement(direction)
case input.KeynavDirectionBackward:
// if we recieve a backward direction, focus the last
// focusable element.
return element.focusLastFocusableElement(direction)
}
} else {
// an element is currently focused, so we need to move the
// focus in the specified direction
firstFocusedChild :=
element.children[firstFocused].Element.(elements.Focusable)
// before we move the focus, the currently focused child
// may also be able to move its focus. if the child is able
// to do that, we will let it and not move ours.
if firstFocusedChild.HandleFocus(direction) {
return true
}
// find the previous/next focusable element relative to the
// currently focused element, if it exists.
for index := firstFocused + int(direction);
index < len(element.children) && index >= 0;
index += int(direction) {
child, focusable :=
element.children[index].
Element.(elements.Focusable)
if focusable && child.HandleFocus(direction) {
// we have found one, so we now actually move
// the focus.
firstFocusedChild.HandleUnfocus()
element.focused = true
return true
}
}
}
return false
}
func (element *Container) focusFirstFocusableElement (
direction input.KeynavDirection,
) (
ok bool,
) {
element.forFocusable (func (child elements.Focusable) bool {
if child.HandleFocus(direction) {
element.focused = true
ok = true
return false
}
return true
})
return
}
func (element *Container) focusLastFocusableElement (
direction input.KeynavDirection,
) (
ok bool,
) {
element.forFocusableBackward (func (child elements.Focusable) bool {
if child.HandleFocus(direction) {
element.focused = true
ok = true
return false
}
return true
})
return
}
func (element *Container) HandleUnfocus () {
element.focused = false
element.forFocused (func (child elements.Focusable) bool {
child.HandleUnfocus()
return true
})
}
func (element *Container) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback
}
func (element *Container) OnFocusMotionRequest (
callback func (direction input.KeynavDirection) (granted bool),
) {
element.onFocusMotionRequest = callback
}
func (element *Container) forFocused (callback func (child elements.Focusable) bool) {
for _, entry := range element.children {
child, focusable := entry.Element.(elements.Focusable)
if focusable && child.Focused() {
if !callback(child) { break }
}
}
}
func (element *Container) forFocusable (callback func (child elements.Focusable) bool) {
for _, entry := range element.children {
child, focusable := entry.Element.(elements.Focusable)
if focusable {
if !callback(child) { break }
}
}
}
func (element *Container) forFlexible (callback func (child elements.Flexible) bool) {
for _, entry := range element.children {
child, flexible := entry.Element.(elements.Flexible)
if flexible {
if !callback(child) { break }
}
}
}
func (element *Container) forFocusableBackward (callback func (child elements.Focusable) bool) {
for index := len(element.children) - 1; index >= 0; index -- {
child, focusable := element.children[index].Element.(elements.Focusable)
if focusable {
if !callback(child) { break }
}
}
}
func (element *Container) firstFocused () (index int) {
for currentIndex, entry := range element.children {
child, focusable := entry.Element.(elements.Focusable)
if focusable && child.Focused() {
return currentIndex
}
}
return -1
}
func (element *Container) reflectChildProperties () {
element.focusable = false
element.forFocusable (func (elements.Focusable) bool {
element.focusable = true
return false
})
element.flexible = false
element.forFlexible (func (elements.Flexible) bool {
element.flexible = true
return false
})
if !element.focusable {
element.focused = false
}
}
func (element *Container) childFocusRequestCallback (
child elements.Focusable,
) (
granted bool,
) {
if element.onFocusRequest != nil && element.onFocusRequest() {
element.focused = true
element.forFocused (func (child elements.Focusable) bool {
child.HandleUnfocus()
return true
})
return true
} else {
return false
}
}
func (element *Container) updateMinimumSize () {
margin := element.theme.Margin(theme.PatternBackground)
// TODO: have layouts take in x and y margins
width, height := element.layout.MinimumSize(element.children, margin.X)
if element.flexible {
height = element.layout.FlexibleHeightFor (
element.children,
margin.X, width)
}
element.core.SetMinimumSize(width, height)
}
func (element *Container) doLayout () {
margin := element.theme.Margin(theme.PatternBackground)
// TODO: have layouts take in x and y margins
element.layout.Arrange (
element.children, margin.X, element.Bounds())
}

View File

@ -1,3 +0,0 @@
// Package basicElements provides standard elements that are commonly used in
// GUI applications.
package basicElements

View File

@ -1,25 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
type Image struct {
*core.Core
core core.CoreControl
buffer canvas.Canvas
}
func NewImage (image image.Image) (element *Image) {
element = &Image { buffer: canvas.FromImage(image) }
element.Core, element.core = core.NewCore(element.draw)
bounds := image.Bounds()
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
return
}
func (element *Image) draw () {
(patterns.Texture { Canvas: element.buffer }).
Draw(element.core, element.Bounds())
}

View File

@ -1,167 +0,0 @@
package basicElements
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Label is a simple text box.
type Label struct {
*core.Core
core core.CoreControl
wrap bool
text string
drawer textdraw.Drawer
config config.Wrapped
theme theme.Wrapped
onFlexibleHeightChange func ()
}
// NewLabel creates a new label. If wrap is set to true, the text inside will be
// wrapped.
func NewLabel (text string, wrap bool) (element *Label) {
element = &Label { }
element.theme.Case = theme.C("basic", "label")
element.Core, element.core = core.NewCore(element.handleResize)
element.SetWrap(wrap)
element.SetText(text)
return
}
func (element *Label) redo () {
face := element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal)
element.drawer.SetFace(face)
element.updateMinimumSize()
bounds := element.Bounds()
if element.wrap {
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.draw()
element.core.DamageAll()
}
func (element *Label) handleResize () {
bounds := element.Bounds()
if element.wrap {
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.draw()
return
}
// FlexibleHeightFor returns the reccomended height for this element based on
// the given width in order to allow the text to wrap properly.
func (element *Label) FlexibleHeightFor (width int) (height int) {
if element.wrap {
return element.drawer.ReccomendedHeightFor(width)
} else {
_, height = element.MinimumSize()
return
}
}
// OnFlexibleHeightChange sets a function to be called when the parameters
// affecting this element's flexible height are changed.
func (element *Label) OnFlexibleHeightChange (callback func ()) {
element.onFlexibleHeightChange = callback
}
// SetText sets the label's text.
func (element *Label) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
// SetWrap sets wether or not the label's text wraps. If the text is set to
// wrap, the element will have a minimum size of a single character and
// automatically wrap its text. If the text is set to not wrap, the element will
// have a minimum size that fits its text.
func (element *Label) SetWrap (wrap bool) {
if wrap == element.wrap { return }
if !wrap {
element.drawer.SetMaxWidth(0)
element.drawer.SetMaxHeight(0)
}
element.wrap = wrap
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
// SetTheme sets the element's theme.
func (element *Label) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
// SetConfig sets the element's configuration.
func (element *Label) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Label) updateMinimumSize () {
if element.wrap {
em := element.drawer.Em().Round()
if em < 1 {
em = element.theme.Padding(theme.PatternBackground)[0]
}
element.core.SetMinimumSize (
em, element.drawer.LineHeight().Round())
if element.onFlexibleHeightChange != nil {
element.onFlexibleHeightChange()
}
} else {
bounds := element.drawer.LayoutBounds()
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
}
}
func (element *Label) draw () {
bounds := element.Bounds()
pattern := element.theme.Pattern (
theme.PatternBackground,
theme.State { })
pattern.Draw(element.core, bounds)
textBounds := element.drawer.LayoutBounds()
foreground := element.theme.Color (
theme.ColorForeground,
theme.State { })
element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min))
}

View File

@ -1,465 +0,0 @@
package basicElements
import "fmt"
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// List is an element that contains several objects that a user can select.
type List struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
pressed bool
contentHeight int
forcedMinimumWidth int
forcedMinimumHeight int
selectedEntry int
scroll int
entries []ListEntry
config config.Wrapped
theme theme.Wrapped
onScrollBoundsChange func ()
onNoEntrySelected func ()
}
// NewList creates a new list element with the specified entries.
func NewList (entries ...ListEntry) (element *List) {
element = &List { selectedEntry: -1 }
element.theme.Case = theme.C("basic", "list")
element.Core, element.core = core.NewCore(element.handleResize)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.entries = make([]ListEntry, len(entries))
for index, entry := range entries {
element.entries[index] = entry
}
element.updateMinimumSize()
return
}
func (element *List) handleResize () {
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
if element.scroll > element.maxScrollHeight() {
element.scroll = element.maxScrollHeight()
}
element.draw()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// SetTheme sets the element's theme.
func (element *List) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
for index, entry := range element.entries {
entry.SetTheme(element.theme.Theme)
element.entries[index] = entry
}
element.updateMinimumSize()
element.redo()
}
// SetConfig sets the element's configuration.
func (element *List) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
for index, entry := range element.entries {
entry.SetConfig(element.config)
element.entries[index] = entry
}
element.updateMinimumSize()
element.redo()
}
func (element *List) redo () {
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// Collapse forces a minimum width and height upon the list. If a zero value is
// given for a dimension, its minimum will be determined by the list's content.
// If the list's height goes beyond the forced size, it will need to be accessed
// via scrolling. If an entry's width goes beyond the forced size, its text will
// be truncated so that it fits.
func (element *List) Collapse (width, height int) {
if
element.forcedMinimumWidth == width &&
element.forcedMinimumHeight == height {
return
}
element.forcedMinimumWidth = width
element.forcedMinimumHeight = height
element.updateMinimumSize()
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
element.redo()
}
func (element *List) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if button != input.ButtonLeft { return }
element.pressed = true
if element.selectUnderMouse(x, y) && element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *List) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
element.pressed = false
}
func (element *List) HandleMouseMove (x, y int) {
if element.pressed {
if element.selectUnderMouse(x, y) && element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
altered := false
switch key {
case input.KeyLeft, input.KeyUp:
altered = element.changeSelectionBy(-1)
case input.KeyRight, input.KeyDown:
altered = element.changeSelectionBy(1)
case input.KeyEscape:
altered = element.selectEntry(-1)
}
if altered && element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
// ScrollContentBounds returns the full content size of the element.
func (element *List) ScrollContentBounds () (bounds image.Rectangle) {
return image.Rect (
0, 0,
1, element.contentHeight)
}
// ScrollViewportBounds returns the size and position of the element's viewport
// relative to ScrollBounds.
func (element *List) ScrollViewportBounds () (bounds image.Rectangle) {
return image.Rect (
0, element.scroll,
0, element.scroll + element.scrollViewportHeight())
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *List) ScrollTo (position image.Point) {
element.scroll = position.Y
if element.scroll < 0 {
element.scroll = 0
} else if element.scroll > element.maxScrollHeight() {
element.scroll = element.maxScrollHeight()
}
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// ScrollAxes returns the supported axes for scrolling.
func (element *List) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *List) scrollViewportHeight () (height int) {
padding := element.theme.Padding(theme.PatternSunken)
return element.Bounds().Dy() - padding[0] - padding[2]
}
func (element *List) maxScrollHeight () (height int) {
height =
element.contentHeight -
element.scrollViewportHeight()
if height < 0 { height = 0 }
return
}
func (element *List) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// OnNoEntrySelected sets a function to be called when the user chooses to
// deselect the current selected entry by clicking on empty space within the
// list or by pressing the escape key.
func (element *List) OnNoEntrySelected (callback func ()) {
element.onNoEntrySelected = callback
}
// CountEntries returns the amount of entries in the list.
func (element *List) CountEntries () (count int) {
return len(element.entries)
}
// Append adds an entry to the end of the list.
func (element *List) Append (entry ListEntry) {
// append
entry = element.resizeEntryToFit(entry)
entry.SetTheme(element.theme.Theme)
entry.SetConfig(element.config)
element.entries = append(element.entries, entry)
// recalculate, redraw, notify
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// EntryAt returns the entry at the specified index. If the index is out of
// bounds, it panics.
func (element *List) EntryAt (index int) (entry ListEntry) {
if index < 0 || index >= len(element.entries) {
panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index))
}
return element.entries[index]
}
// Insert inserts an entry into the list at the speified index. If the index is
// out of bounds, it is constrained either to zero or len(entries).
func (element *List) Insert (index int, entry ListEntry) {
if index < 0 { index = 0 }
if index > len(element.entries) { index = len(element.entries) }
// insert
element.entries = append (
element.entries[:index + 1],
element.entries[index:]...)
entry = element.resizeEntryToFit(entry)
element.entries[index] = entry
// recalculate, redraw, notify
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// Remove removes the entry at the specified index. If the index is out of
// bounds, it panics.
func (element *List) Remove (index int) {
if index < 0 || index >= len(element.entries) {
panic(fmt.Sprint("basic.List.Remove index out of range: ", index))
}
// delete
element.entries = append (
element.entries[:index],
element.entries[index + 1:]...)
// recalculate, redraw, notify
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// Replace replaces the entry at the specified index with another. If the index
// is out of bounds, it panics.
func (element *List) Replace (index int, entry ListEntry) {
if index < 0 || index >= len(element.entries) {
panic(fmt.Sprint("basic.List.Replace index out of range: ", index))
}
// replace
entry = element.resizeEntryToFit(entry)
element.entries[index] = entry
// redraw
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// Select selects a specific item in the list. If the index is out of bounds,
// no items will be selecected.
func (element *List) Select (index int) {
if element.selectEntry(index) {
element.redo()
}
}
func (element *List) selectUnderMouse (x, y int) (updated bool) {
padding := element.theme.Padding(theme.PatternSunken)
bounds := padding.Apply(element.Bounds())
mousePoint := image.Pt(x, y)
dot := image.Pt (
bounds.Min.X,
bounds.Min.Y - element.scroll)
newlySelectedEntryIndex := -1
for index, entry := range element.entries {
entryPosition := dot
dot.Y += entry.Bounds().Dy()
if entryPosition.Y > bounds.Max.Y { break }
if mousePoint.In(entry.Bounds().Add(entryPosition)) {
newlySelectedEntryIndex = index
break
}
}
return element.selectEntry(newlySelectedEntryIndex)
}
func (element *List) selectEntry (index int) (updated bool) {
if element.selectedEntry == index { return false }
element.selectedEntry = index
if element.selectedEntry < 0 {
if element.onNoEntrySelected != nil {
element.onNoEntrySelected()
}
} else {
element.entries[element.selectedEntry].RunSelect()
}
return true
}
func (element *List) changeSelectionBy (delta int) (updated bool) {
newIndex := element.selectedEntry + delta
if newIndex < 0 { newIndex = len(element.entries) - 1 }
if newIndex >= len(element.entries) { newIndex = 0 }
return element.selectEntry(newIndex)
}
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
bounds := element.Bounds()
padding := element.theme.Padding(theme.PatternSunken)
entry.Resize(padding.Apply(bounds).Dx())
return entry
}
func (element *List) updateMinimumSize () {
element.contentHeight = 0
for _, entry := range element.entries {
element.contentHeight += entry.Bounds().Dy()
}
minimumWidth := element.forcedMinimumWidth
minimumHeight := element.forcedMinimumHeight
if minimumWidth == 0 {
for _, entry := range element.entries {
entryWidth := entry.MinimumWidth()
if entryWidth > minimumWidth {
minimumWidth = entryWidth
}
}
}
if minimumHeight == 0 {
minimumHeight = element.contentHeight
}
padding := element.theme.Padding(theme.PatternSunken)
minimumHeight += padding[0] + padding[2]
element.core.SetMinimumSize(minimumWidth, minimumHeight)
}
func (element *List) draw () {
bounds := element.Bounds()
padding := element.theme.Padding(theme.PatternSunken)
innerBounds := padding.Apply(bounds)
state := theme.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
}
dot := image.Point {
innerBounds.Min.X,
innerBounds.Min.Y - element.scroll,
}
innerCanvas := canvas.Cut(element.core, innerBounds)
for index, entry := range element.entries {
entryPosition := dot
dot.Y += entry.Bounds().Dy()
if dot.Y < innerBounds.Min.Y { continue }
if entryPosition.Y > innerBounds.Max.Y { break }
entry.Draw (
innerCanvas, entryPosition,
element.Focused(), element.selectedEntry == index)
}
covered := image.Rect (
0, 0,
innerBounds.Dx(), element.contentHeight,
).Add(innerBounds.Min).Intersect(innerBounds)
pattern := element.theme.Pattern(theme.PatternSunken, state)
artist.DrawShatter (
element.core, pattern, covered)
}

View File

@ -1,100 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
// ListEntry is an item that can be added to a list.
type ListEntry struct {
drawer textdraw.Drawer
bounds image.Rectangle
text string
width int
minimumWidth int
config config.Wrapped
theme theme.Wrapped
onSelect func ()
}
func NewListEntry (text string, onSelect func ()) (entry ListEntry) {
entry = ListEntry {
text: text,
onSelect: onSelect,
}
entry.theme.Case = theme.C("basic", "listEntry")
entry.drawer.SetText([]rune(text))
entry.updateBounds()
return
}
func (entry *ListEntry) SetTheme (new theme.Theme) {
if new == entry.theme.Theme { return }
entry.theme.Theme = new
entry.drawer.SetFace (entry.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal))
entry.updateBounds()
}
func (entry *ListEntry) SetConfig (new config.Config) {
if new == entry.config.Config { return }
entry.config.Config = new
}
func (entry *ListEntry) updateBounds () {
padding := entry.theme.Padding(theme.PatternRaised)
entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds())
entry.bounds = entry.bounds.Sub(entry.bounds.Min)
entry.minimumWidth = entry.bounds.Dx()
entry.bounds.Max.X = entry.width
}
func (entry *ListEntry) Draw (
destination canvas.Canvas,
offset image.Point,
focused bool,
on bool,
) (
updatedRegion image.Rectangle,
) {
state := theme.State {
Focused: focused,
On: on,
}
pattern := entry.theme.Pattern(theme.PatternRaised, state)
padding := entry.theme.Padding(theme.PatternRaised)
bounds := entry.Bounds().Add(offset)
artist.DrawBounds(destination, pattern, bounds)
foreground := entry.theme.Color (theme.ColorForeground, state)
return entry.drawer.Draw (
destination,
foreground,
offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])).
Sub(entry.drawer.LayoutBounds().Min))
}
func (entry *ListEntry) RunSelect () {
if entry.onSelect != nil {
entry.onSelect()
}
}
func (entry *ListEntry) Bounds () (bounds image.Rectangle) {
return entry.bounds
}
func (entry *ListEntry) Resize (width int) {
entry.width = width
entry.updateBounds()
}
func (entry *ListEntry) MinimumWidth () (width int) {
return entry.minimumWidth
}

View File

@ -1,82 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// ProgressBar displays a visual indication of how far along a task is.
type ProgressBar struct {
*core.Core
core core.CoreControl
progress float64
config config.Wrapped
theme theme.Wrapped
}
// NewProgressBar creates a new progress bar displaying the given progress
// level.
func NewProgressBar (progress float64) (element *ProgressBar) {
element = &ProgressBar { progress: progress }
element.theme.Case = theme.C("basic", "progressBar")
element.Core, element.core = core.NewCore(element.draw)
return
}
// SetProgress sets the progress level of the bar.
func (element *ProgressBar) SetProgress (progress float64) {
if progress == element.progress { return }
element.progress = progress
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
// SetTheme sets the element's theme.
func (element *ProgressBar) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.updateMinimumSize()
element.redo()
}
// SetConfig sets the element's configuration.
func (element *ProgressBar) SetConfig (new config.Config) {
if new == nil || new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
}
func (element (ProgressBar)) updateMinimumSize() {
padding := element.theme.Padding(theme.PatternSunken)
innerPadding := element.theme.Padding(theme.PatternMercury)
element.core.SetMinimumSize (
padding.Horizontal() + innerPadding.Horizontal(),
padding.Vertical() + innerPadding.Vertical())
}
func (element *ProgressBar) redo () {
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *ProgressBar) draw () {
bounds := element.Bounds()
pattern := element.theme.Pattern(theme.PatternSunken, theme.State { })
padding := element.theme.Padding(theme.PatternSunken)
pattern.Draw(element.core, bounds)
bounds = padding.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
mercury := element.theme.Pattern(theme.PatternMercury, theme.State { })
artist.DrawBounds(element.core, mercury, meterBounds)
}

View File

@ -1,528 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// ScrollContainer is a container that is capable of holding a scrollable
// element.
type ScrollContainer struct {
*core.Core
core core.CoreControl
focused bool
child elements.Scrollable
childWidth, childHeight int
horizontal struct {
theme theme.Wrapped
exists bool
enabled bool
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
vertical struct {
theme theme.Wrapped
exists bool
enabled bool
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
config config.Wrapped
theme theme.Wrapped
onFocusRequest func () (granted bool)
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
}
// NewScrollContainer creates a new scroll container with the specified scroll
// bars.
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
element = &ScrollContainer { }
element.theme.Case = theme.C("basic", "scrollContainer")
element.horizontal.theme.Case = theme.C("basic", "scrollBarHorizontal")
element.vertical.theme.Case = theme.C("basic", "scrollBarVertical")
element.Core, element.core = core.NewCore(element.handleResize)
element.horizontal.exists = horizontal
element.vertical.exists = vertical
return
}
func (element *ScrollContainer) handleResize () {
element.recalculate()
element.resizeChildToFit()
element.draw()
}
// Adopt adds a scrollable element to the scroll container. The container can
// only contain one scrollable element at a time, and when a new one is adopted
// it replaces the last one.
func (element *ScrollContainer) Adopt (child elements.Scrollable) {
// disown previous child if it exists
if element.child != nil {
element.clearChildEventHandlers(child)
}
// adopt new child
element.child = child
if child != nil {
if child0, ok := child.(elements.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
if child0, ok := child.(elements.Configurable); ok {
child0.SetConfig(element.config.Config)
}
child.OnDamage(element.childDamageCallback)
child.OnMinimumSizeChange(element.updateMinimumSize)
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
if newChild, ok := child.(elements.Focusable); ok {
newChild.OnFocusRequest (
element.childFocusRequestCallback)
newChild.OnFocusMotionRequest (
element.childFocusMotionRequestCallback)
}
element.updateMinimumSize()
element.horizontal.enabled,
element.vertical.enabled = element.child.ScrollAxes()
if element.core.HasImage() {
element.resizeChildToFit()
}
}
}
// SetTheme sets the element's theme.
func (element *ScrollContainer) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
if child, ok := element.child.(elements.Themeable); ok {
child.SetTheme(element.theme.Theme)
}
if element.core.HasImage() {
element.recalculate()
element.resizeChildToFit()
element.draw()
}
}
// SetConfig sets the element's configuration.
func (element *ScrollContainer) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
if child, ok := element.child.(elements.Configurable); ok {
child.SetConfig(element.config.Config)
}
if element.core.HasImage() {
element.recalculate()
element.resizeChildToFit()
element.draw()
}
}
func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if child, ok := element.child.(elements.KeyboardTarget); ok {
child.HandleKeyDown(key, modifiers)
}
}
func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if child, ok := element.child.(elements.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers)
}
}
func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) {
velocity := element.config.ScrollVelocity()
point := image.Pt(x, y)
if point.In(element.horizontal.bar) {
element.horizontal.dragging = true
element.horizontal.dragOffset =
x - element.horizontal.bar.Min.X +
element.Bounds().Min.X
element.dragHorizontalBar(point)
} else if point.In(element.horizontal.gutter) {
switch button {
case input.ButtonLeft:
element.horizontal.dragging = true
element.horizontal.dragOffset =
element.horizontal.bar.Dx() / 2 +
element.Bounds().Min.X
element.dragHorizontalBar(point)
case input.ButtonMiddle:
viewport := element.child.ScrollViewportBounds().Dx()
if x > element.horizontal.bar.Min.X {
element.scrollChildBy(viewport, 0)
} else {
element.scrollChildBy(-viewport, 0)
}
case input.ButtonRight:
if x > element.horizontal.bar.Min.X {
element.scrollChildBy(velocity, 0)
} else {
element.scrollChildBy(-velocity, 0)
}
}
} else if point.In(element.vertical.bar) {
element.vertical.dragging = true
element.vertical.dragOffset =
y - element.vertical.bar.Min.Y +
element.Bounds().Min.Y
element.dragVerticalBar(point)
} else if point.In(element.vertical.gutter) {
switch button {
case input.ButtonLeft:
element.vertical.dragging = true
element.vertical.dragOffset =
element.vertical.bar.Dy() / 2 +
element.Bounds().Min.Y
element.dragVerticalBar(point)
case input.ButtonMiddle:
viewport := element.child.ScrollViewportBounds().Dy()
if y > element.vertical.bar.Min.Y {
element.scrollChildBy(0, viewport)
} else {
element.scrollChildBy(0, -viewport)
}
case input.ButtonRight:
if y > element.vertical.bar.Min.Y {
element.scrollChildBy(0, velocity)
} else {
element.scrollChildBy(0, -velocity)
}
}
} else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseDown(x, y, button)
}
}
func (element *ScrollContainer) HandleMouseUp (x, y int, button input.Button) {
if element.horizontal.dragging {
element.horizontal.dragging = false
element.drawHorizontalBar()
element.core.DamageRegion(element.horizontal.bar)
} else if element.vertical.dragging {
element.vertical.dragging = false
element.drawVerticalBar()
element.core.DamageRegion(element.vertical.bar)
} else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseUp(x, y, button)
}
}
func (element *ScrollContainer) HandleMouseMove (x, y int) {
if element.horizontal.dragging {
element.dragHorizontalBar(image.Pt(x, y))
} else if element.vertical.dragging {
element.dragVerticalBar(image.Pt(x, y))
} else if child, ok := element.child.(elements.MouseTarget); ok {
child.HandleMouseMove(x, y)
}
}
func (element *ScrollContainer) HandleMouseScroll (
x, y int,
deltaX, deltaY float64,
) {
element.scrollChildBy(int(deltaX), int(deltaY))
}
func (element *ScrollContainer) scrollChildBy (x, y int) {
if element.child == nil { return }
scrollPoint :=
element.child.ScrollViewportBounds().Min.
Add(image.Pt(x, y))
element.child.ScrollTo(scrollPoint)
}
func (element *ScrollContainer) Focused () (focused bool) {
return element.focused
}
func (element *ScrollContainer) Focus () {
if element.onFocusRequest != nil {
if element.onFocusRequest() {
element.focused = true
}
}
}
func (element *ScrollContainer) HandleFocus (
direction input.KeynavDirection,
) (
accepted bool,
) {
if child, ok := element.child.(elements.Focusable); ok {
element.focused = child.HandleFocus(direction)
return element.focused
} else {
element.focused = false
return false
}
}
func (element *ScrollContainer) HandleUnfocus () {
if child, ok := element.child.(elements.Focusable); ok {
child.HandleUnfocus()
}
element.focused = false
}
func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback
}
func (element *ScrollContainer) OnFocusMotionRequest (
callback func (direction input.KeynavDirection) (granted bool),
) {
element.onFocusMotionRequest = callback
}
func (element *ScrollContainer) childDamageCallback (region canvas.Canvas) {
element.core.DamageRegion(region.Bounds())
}
func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
if element.onFocusRequest != nil {
element.focused = element.onFocusRequest()
return element.focused
} else {
return false
}
}
func (element *ScrollContainer) childFocusMotionRequestCallback (
direction input.KeynavDirection,
) (
granted bool,
) {
if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction)
}
func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) {
child.DrawTo(nil)
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
child.OnScrollBoundsChange(nil)
if child0, ok := child.(elements.Focusable); ok {
child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil)
if child0.Focused() {
child0.HandleUnfocus()
}
}
if child0, ok := child.(elements.Flexible); ok {
child0.OnFlexibleHeightChange(nil)
}
}
func (element *ScrollContainer) resizeChildToFit () {
childBounds := image.Rect (
0, 0,
element.childWidth,
element.childHeight).Add(element.Bounds().Min)
element.child.DrawTo(canvas.Cut(element.core, childBounds))
}
func (element *ScrollContainer) recalculate () {
horizontal := &element.horizontal
vertical := &element.vertical
gutterInsetHorizontal := horizontal.theme.Padding(theme.PatternGutter)
gutterInsetVertical := vertical.theme.Padding(theme.PatternGutter)
bounds := element.Bounds()
thicknessHorizontal :=
element.config.HandleWidth() +
gutterInsetHorizontal[3] +
gutterInsetHorizontal[1]
thicknessVertical :=
element.config.HandleWidth() +
gutterInsetVertical[3] +
gutterInsetVertical[1]
// calculate child size
element.childWidth = bounds.Dx()
element.childHeight = bounds.Dy()
// reset bounds
horizontal.gutter = image.Rectangle { }
vertical.gutter = image.Rectangle { }
horizontal.bar = image.Rectangle { }
vertical.bar = image.Rectangle { }
// if enabled, give substance to the gutters
if horizontal.exists {
horizontal.gutter.Min.X = bounds.Min.X
horizontal.gutter.Min.Y = bounds.Max.Y - thicknessHorizontal
horizontal.gutter.Max.X = bounds.Max.X
horizontal.gutter.Max.Y = bounds.Max.Y
if vertical.exists {
horizontal.gutter.Max.X -= thicknessVertical
}
element.childHeight -= thicknessHorizontal
horizontal.track = gutterInsetHorizontal.Apply(horizontal.gutter)
}
if vertical.exists {
vertical.gutter.Min.X = bounds.Max.X - thicknessVertical
vertical.gutter.Max.X = bounds.Max.X
vertical.gutter.Min.Y = bounds.Min.Y
vertical.gutter.Max.Y = bounds.Max.Y
if horizontal.exists {
vertical.gutter.Max.Y -= thicknessHorizontal
}
element.childWidth -= thicknessVertical
vertical.track = gutterInsetVertical.Apply(vertical.gutter)
}
// if enabled, calculate the positions of the bars
contentBounds := element.child.ScrollContentBounds()
viewportBounds := element.child.ScrollViewportBounds()
if horizontal.exists && horizontal.enabled {
horizontal.bar.Min.Y = horizontal.track.Min.Y
horizontal.bar.Max.Y = horizontal.track.Max.Y
scale := float64(horizontal.track.Dx()) /
float64(contentBounds.Dx())
horizontal.bar.Min.X = int(float64(viewportBounds.Min.X) * scale)
horizontal.bar.Max.X = int(float64(viewportBounds.Max.X) * scale)
horizontal.bar.Min.X += horizontal.track.Min.X
horizontal.bar.Max.X += horizontal.track.Min.X
}
if vertical.exists && vertical.enabled {
vertical.bar.Min.X = vertical.track.Min.X
vertical.bar.Max.X = vertical.track.Max.X
scale := float64(vertical.track.Dy()) /
float64(contentBounds.Dy())
vertical.bar.Min.Y = int(float64(viewportBounds.Min.Y) * scale)
vertical.bar.Max.Y = int(float64(viewportBounds.Max.Y) * scale)
vertical.bar.Min.Y += vertical.track.Min.Y
vertical.bar.Max.Y += vertical.track.Min.Y
}
// if the scroll bars are out of bounds, don't display them.
if horizontal.bar.Dx() >= horizontal.track.Dx() {
horizontal.bar = image.Rectangle { }
}
if vertical.bar.Dy() >= vertical.track.Dy() {
vertical.bar = image.Rectangle { }
}
}
func (element *ScrollContainer) draw () {
deadPattern := element.theme.Pattern (
theme.PatternDead, theme.State { })
artist.DrawBounds (
element.core, deadPattern,
image.Rect (
element.vertical.gutter.Min.X,
element.horizontal.gutter.Min.Y,
element.vertical.gutter.Max.X,
element.horizontal.gutter.Max.Y))
element.drawHorizontalBar()
element.drawVerticalBar()
}
func (element *ScrollContainer) drawHorizontalBar () {
state := theme.State {
Disabled: !element.horizontal.enabled,
Pressed: element.horizontal.dragging,
}
gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state)
artist.DrawBounds(element.core, gutterPattern, element.horizontal.gutter)
handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state)
artist.DrawBounds(element.core, handlePattern, element.horizontal.bar)
}
func (element *ScrollContainer) drawVerticalBar () {
state := theme.State {
Disabled: !element.vertical.enabled,
Pressed: element.vertical.dragging,
}
gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state)
artist.DrawBounds(element.core, gutterPattern, element.vertical.gutter)
handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state)
artist.DrawBounds(element.core, handlePattern, element.vertical.bar)
}
func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
scrollX :=
float64(element.child.ScrollContentBounds().Dx()) /
float64(element.horizontal.track.Dx()) *
float64(mousePosition.X - element.horizontal.dragOffset)
scrollY := element.child.ScrollViewportBounds().Min.Y
element.child.ScrollTo(image.Pt(int(scrollX), scrollY))
}
func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
scrollY :=
float64(element.child.ScrollContentBounds().Dy()) /
float64(element.vertical.track.Dy()) *
float64(mousePosition.Y - element.vertical.dragOffset)
scrollX := element.child.ScrollViewportBounds().Min.X
element.child.ScrollTo(image.Pt(scrollX, int(scrollY)))
}
func (element *ScrollContainer) updateMinimumSize () {
gutterInsetHorizontal := element.horizontal.theme.Padding(theme.PatternGutter)
gutterInsetVertical := element.vertical.theme.Padding(theme.PatternGutter)
thicknessHorizontal :=
element.config.HandleWidth() +
gutterInsetHorizontal[3] +
gutterInsetHorizontal[1]
thicknessVertical :=
element.config.HandleWidth() +
gutterInsetVertical[3] +
gutterInsetVertical[1]
width := thicknessHorizontal
height := thicknessVertical
if element.child != nil {
childWidth, childHeight := element.child.MinimumSize()
width += childWidth
height += childHeight
}
element.core.SetMinimumSize(width, height)
}
func (element *ScrollContainer) childScrollBoundsChangeCallback () {
element.horizontal.enabled,
element.vertical.enabled = element.child.ScrollAxes()
if element.core.HasImage() {
element.recalculate()
element.drawHorizontalBar()
element.drawVerticalBar()
element.core.DamageRegion(element.horizontal.gutter)
element.core.DamageRegion(element.vertical.gutter)
}
}

View File

@ -1,206 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Slider is a slider control with a floating point value between zero and one.
type Slider struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
value float64
vertical bool
dragging bool
dragOffset int
track image.Rectangle
bar image.Rectangle
config config.Wrapped
theme theme.Wrapped
onSlide func ()
onRelease func ()
}
// NewSlider creates a new slider with the specified value. If vertical is set
// to true,
func NewSlider (value float64, vertical bool) (element *Slider) {
element = &Slider {
value: value,
vertical: vertical,
}
if vertical {
element.theme.Case = theme.C("basic", "sliderVertical")
} else {
element.theme.Case = theme.C("basic", "sliderHorizontal")
}
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.redo)
element.updateMinimumSize()
return
}
func (element *Slider) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
if button == input.ButtonLeft {
element.dragging = true
element.value = element.valueFor(x, y)
if element.onSlide != nil {
element.onSlide()
}
element.redo()
}
}
func (element *Slider) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft || !element.dragging { return }
element.dragging = false
if element.onRelease != nil {
element.onRelease()
}
element.redo()
}
func (element *Slider) HandleMouseMove (x, y int) {
if element.dragging {
element.dragging = true
element.value = element.valueFor(x, y)
if element.onSlide != nil {
element.onSlide()
}
element.redo()
}
}
func (element *Slider) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Slider) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
// TODO: handle left and right arrows
}
func (element *Slider) HandleKeyUp (key input.Key, modifiers input.Modifiers) { }
// Value returns the slider's value.
func (element *Slider) Value () (value float64) {
return element.value
}
// SetEnabled sets whether or not the slider can be interacted with.
func (element *Slider) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
}
// SetValue sets the slider's value.
func (element *Slider) SetValue (value float64) {
if value < 0 { value = 0 }
if value > 1 { value = 1 }
if element.value == value { return }
element.value = value
element.redo()
}
// OnSlide sets a function to be called every time the slider handle changes
// position while being dragged.
func (element *Slider) OnSlide (callback func ()) {
element.onSlide = callback
}
// OnRelease sets a function to be called when the handle stops being dragged.
func (element *Slider) OnRelease (callback func ()) {
element.onRelease = callback
}
// SetTheme sets the element's theme.
func (element *Slider) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.redo()
}
// SetConfig sets the element's configuration.
func (element *Slider) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
}
func (element *Slider) valueFor (x, y int) (value float64) {
if element.vertical {
value =
float64(y - element.track.Min.Y - element.bar.Dy() / 2) /
float64(element.track.Dy() - element.bar.Dy())
value = 1 - value
} else {
value =
float64(x - element.track.Min.X - element.bar.Dx() / 2) /
float64(element.track.Dx() - element.bar.Dx())
}
if value < 0 { value = 0 }
if value > 1 { value = 1 }
return
}
func (element *Slider) updateMinimumSize () {
if element.vertical {
element.core.SetMinimumSize (
element.config.HandleWidth(),
element.config.HandleWidth() * 2)
} else {
element.core.SetMinimumSize (
element.config.HandleWidth() * 2,
element.config.HandleWidth())
}
}
func (element *Slider) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Slider) draw () {
bounds := element.Bounds()
element.track = element.theme.Padding(theme.PatternGutter).Apply(bounds)
if element.vertical {
barSize := element.track.Dx()
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
barOffset :=
float64(element.track.Dy() - barSize) *
(1 - element.value)
element.bar = element.bar.Add(image.Pt(0, int(barOffset)))
} else {
barSize := element.track.Dy()
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
barOffset :=
float64(element.track.Dx() - barSize) *
element.value
element.bar = element.bar.Add(image.Pt(int(barOffset), 0))
}
state := theme.State {
Focused: element.Focused(),
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
artist.DrawBounds (
element.core,
element.theme.Pattern(theme.PatternGutter, state),
bounds)
artist.DrawBounds (
element.core,
element.theme.Pattern(theme.PatternHandle, state),
element.bar)
}

View File

@ -1,85 +0,0 @@
package basicElements
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Spacer can be used to put space between two elements..
type Spacer struct {
*core.Core
core core.CoreControl
line bool
config config.Wrapped
theme theme.Wrapped
}
// NewSpacer creates a new spacer. If line is set to true, the spacer will be
// filled with a line color, and if compressed to its minimum width or height,
// will appear as a line.
func NewSpacer (line bool) (element *Spacer) {
element = &Spacer { line: line }
element.theme.Case = theme.C("basic", "spacer")
element.Core, element.core = core.NewCore(element.draw)
element.updateMinimumSize()
return
}
/// SetLine sets whether or not the spacer will appear as a colored line.
func (element *Spacer) SetLine (line bool) {
if element.line == line { return }
element.line = line
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
// SetTheme sets the element's theme.
func (element *Spacer) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.redo()
}
// SetConfig sets the element's configuration.
func (element *Spacer) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.redo()
}
func (element *Spacer) updateMinimumSize () {
if element.line {
padding := element.theme.Padding(theme.PatternLine)
element.core.SetMinimumSize (
padding.Horizontal(),
padding.Vertical())
} else {
element.core.SetMinimumSize(1, 1)
}
}
func (element *Spacer) redo () {
if !element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Spacer) draw () {
bounds := element.Bounds()
if element.line {
pattern := element.theme.Pattern (
theme.PatternLine,
theme.State { })
pattern.Draw(element.core, bounds)
} else {
pattern := element.theme.Pattern (
theme.PatternBackground,
theme.State { })
pattern.Draw(element.core, bounds)
}
}

View File

@ -1,206 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Switch is a toggle-able on/off switch with an optional label. It is
// functionally identical to Checkbox, but plays a different semantic role.
type Switch struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
drawer textdraw.Drawer
pressed bool
checked bool
text string
config config.Wrapped
theme theme.Wrapped
onToggle func ()
}
// NewSwitch creates a new switch with the specified label text.
func NewSwitch (text string, on bool) (element *Switch) {
element = &Switch {
checked: on,
text: text,
}
element.theme.Case = theme.C("basic", "switch")
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.redo)
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
return
}
func (element *Switch) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
element.redo()
}
func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false
within := image.Point { x, y }.
In(element.Bounds())
if within {
element.checked = !element.checked
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Switch) HandleMouseMove (x, y int) { }
func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
element.redo()
}
}
func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
element.redo()
if element.onToggle != nil {
element.onToggle()
}
}
}
// OnToggle sets the function to be called when the switch is flipped.
func (element *Switch) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the switch is currently on.
func (element *Switch) Value () (on bool) {
return element.checked
}
// SetEnabled sets whether this switch can be flipped or not.
func (element *Switch) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the checkbox's label text.
func (element *Switch) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.redo()
}
// SetTheme sets the element's theme.
func (element *Switch) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal))
element.updateMinimumSize()
element.redo()
}
// SetConfig sets the element's configuration.
func (element *Switch) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
}
func (element *Switch) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Switch) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
lineHeight := element.drawer.LineHeight().Round()
if element.text == "" {
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
} else {
element.core.SetMinimumSize (
lineHeight * 2 +
element.theme.Margin(theme.PatternBackground).X +
textBounds.Dx(),
lineHeight)
}
}
func (element *Switch) draw () {
bounds := element.Bounds()
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
state := theme.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
}
backgroundPattern := element.theme.Pattern (
theme.PatternBackground, state)
backgroundPattern.Draw(element.core, bounds)
if element.checked {
handleBounds.Min.X += bounds.Dy()
handleBounds.Max.X += bounds.Dy()
if element.pressed {
handleBounds.Min.X -= 2
handleBounds.Max.X -= 2
}
} else {
if element.pressed {
handleBounds.Min.X += 2
handleBounds.Max.X += 2
}
}
gutterPattern := element.theme.Pattern (
theme.PatternGutter, state)
artist.DrawBounds(element.core, gutterPattern, gutterBounds)
handlePattern := element.theme.Pattern (
theme.PatternHandle, state)
artist.DrawBounds(element.core, handlePattern, handleBounds)
textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() * 2 +
element.theme.Margin(theme.PatternBackground).X,
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.theme.Color (
theme.ColorForeground, state)
element.drawer.Draw(element.core, foreground, offset)
}

View File

@ -1,418 +0,0 @@
package basicElements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// TextBox is a single-line text input.
type TextBox struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
dragging bool
dot textmanip.Dot
scroll int
placeholder string
text []rune
placeholderDrawer textdraw.Drawer
valueDrawer textdraw.Drawer
config config.Wrapped
theme theme.Wrapped
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
onChange func ()
onScrollBoundsChange func ()
}
// NewTextBox creates a new text box with the specified placeholder text, and
// a value. When the value is empty, the placeholder will be displayed in gray
// text.
func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { }
element.theme.Case = theme.C("basic", "textBox")
element.Core, element.core = core.NewCore(element.handleResize)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.placeholder = placeholder
element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize()
element.SetValue(value)
return
}
func (element *TextBox) handleResize () {
element.scrollToCursor()
element.draw()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *TextBox) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if button == input.ButtonLeft {
runeIndex := element.atPosition(image.Pt(x, y))
element.dragging = true
if runeIndex > -1 {
element.dot = textmanip.EmptyDot(runeIndex)
element.redo()
}
}
}
func (element *TextBox) HandleMouseMove (x, y int) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if element.dragging {
runeIndex := element.atPosition(image.Pt(x, y))
if runeIndex > -1 {
element.dot.End = runeIndex
element.redo()
}
}
}
func (element *TextBox) atPosition (position image.Point) int {
padding := element.theme.Padding(theme.PatternInput)
offset := element.Bounds().Min.Add (image.Pt (
padding[artist.SideLeft] - element.scroll,
padding[artist.SideTop]))
textBoundsMin := element.valueDrawer.LayoutBounds().Min
return element.valueDrawer.AtPosition (
fixedutil.Pt(position.Sub(offset).Add(textBoundsMin)))
}
func (element *TextBox) HandleMouseUp (x, y int, button input.Button) {
if button == input.ButtonLeft {
element.dragging = false
}
}
func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) {
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
return
}
// TODO: text selection with shift
scrollMemory := element.scroll
altered := true
textChanged := false
switch {
case key == input.KeyBackspace:
if len(element.text) < 1 { break }
element.text, element.dot = textmanip.Backspace (
element.text,
element.dot,
modifiers.Control)
textChanged = true
case key == input.KeyDelete:
if len(element.text) < 1 { break }
element.text, element.dot = textmanip.Delete (
element.text,
element.dot,
modifiers.Control)
textChanged = true
case key == input.KeyLeft:
if modifiers.Shift {
element.dot = textmanip.SelectLeft (
element.text,
element.dot,
modifiers.Control)
} else {
element.dot = textmanip.MoveLeft (
element.text,
element.dot,
modifiers.Control)
}
case key == input.KeyRight:
if modifiers.Shift {
element.dot = textmanip.SelectRight (
element.text,
element.dot,
modifiers.Control)
} else {
element.dot = textmanip.MoveRight (
element.text,
element.dot,
modifiers.Control)
}
case key.Printable():
element.text, element.dot = textmanip.Type (
element.text,
element.dot,
rune(key))
textChanged = true
default:
altered = false
}
if textChanged {
element.runOnChange()
element.valueDrawer.SetText(element.text)
}
if altered {
element.scrollToCursor()
}
if (textChanged || scrollMemory != element.scroll) &&
element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
if altered {
element.redo()
}
}
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
func (element *TextBox) SetPlaceholder (placeholder string) {
if element.placeholder == placeholder { return }
element.placeholder = placeholder
element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize()
element.redo()
}
func (element *TextBox) SetValue (text string) {
// if element.text == text { return }
element.text = []rune(text)
element.runOnChange()
element.valueDrawer.SetText(element.text)
if element.dot.End > element.valueDrawer.Length() {
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
}
element.scrollToCursor()
element.redo()
}
func (element *TextBox) Value () (value string) {
return string(element.text)
}
func (element *TextBox) Filled () (filled bool) {
return len(element.text) > 0
}
func (element *TextBox) OnKeyDown (
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
) {
element.onKeyDown = callback
}
func (element *TextBox) OnChange (callback func ()) {
element.onChange = callback
}
// ScrollContentBounds returns the full content size of the element.
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
bounds = element.valueDrawer.LayoutBounds()
return bounds.Sub(bounds.Min)
}
// ScrollViewportBounds returns the size and position of the element's viewport
// relative to ScrollBounds.
func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
return image.Rect (
element.scroll,
0,
element.scroll + element.scrollViewportWidth(),
0)
}
func (element *TextBox) scrollViewportWidth () (width int) {
padding := element.theme.Padding(theme.PatternInput)
return padding.Apply(element.Bounds()).Dx()
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *TextBox) ScrollTo (position image.Point) {
// constrain to minimum
element.scroll = position.X
if element.scroll < 0 { element.scroll = 0 }
// constrain to maximum
contentBounds := element.ScrollContentBounds()
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
if element.scroll > maxPosition { element.scroll = maxPosition }
element.redo()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// ScrollAxes returns the supported axes for scrolling.
func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
return true, false
}
func (element *TextBox) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
func (element *TextBox) runOnChange () {
if element.onChange != nil {
element.onChange()
}
}
func (element *TextBox) scrollToCursor () {
if !element.core.HasImage() { return }
padding := element.theme.Padding(theme.PatternInput)
bounds := padding.Apply(element.Bounds())
bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round()
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
cursorPosition.X -= element.scroll
maxX := bounds.Max.X
minX := maxX
if cursorPosition.X > maxX {
element.scroll += cursorPosition.X - maxX
} else if cursorPosition.X < minX {
element.scroll -= minX - cursorPosition.X
if element.scroll < 0 { element.scroll = 0 }
}
}
// SetTheme sets the element's theme.
func (element *TextBox) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
face := element.theme.FontFace (
theme.FontStyleRegular,
theme.FontSizeNormal)
element.placeholderDrawer.SetFace(face)
element.valueDrawer.SetFace(face)
element.updateMinimumSize()
element.redo()
}
// SetConfig sets the element's configuration.
func (element *TextBox) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
}
func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds()
padding := element.theme.Padding(theme.PatternInput)
element.core.SetMinimumSize (
padding.Horizontal() + textBounds.Dx(),
padding.Vertical() +
element.placeholderDrawer.LineHeight().Round())
}
func (element *TextBox) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *TextBox) draw () {
bounds := element.Bounds()
state := theme.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
}
pattern := element.theme.Pattern(theme.PatternInput, state)
padding := element.theme.Padding(theme.PatternInput)
innerCanvas := canvas.Cut(element.core, padding.Apply(bounds))
pattern.Draw(element.core, bounds)
offset := bounds.Min.Add (image.Point {
X: padding[artist.SideLeft] - element.scroll,
Y: padding[artist.SideTop],
})
if element.Focused() && !element.dot.Empty() {
// draw selection bounds
accent := element.theme.Color(theme.ColorAccent, state)
canon := element.dot.Canon()
foff := fixedutil.Pt(offset)
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
end := element.valueDrawer.PositionAt(canon.End).Add(foff)
end.Y += element.valueDrawer.LineHeight()
shapes.FillColorRectangle (
innerCanvas,
accent,
image.Rectangle {
fixedutil.RoundPt(start),
fixedutil.RoundPt(end),
})
}
if len(element.text) == 0 {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
foreground := element.theme.Color (
theme.ColorForeground,
theme.State { Disabled: true })
element.placeholderDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
} else {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
foreground := element.theme.Color(theme.ColorForeground, state)
element.valueDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
}
if element.Focused() && element.dot.Empty() {
// draw cursor
foreground := element.theme.Color(theme.ColorForeground, state)
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
shapes.ColorLine (
innerCanvas,
foreground, 1,
cursorPosition.Add(offset),
image.Pt (
cursorPosition.X,
cursorPosition.Y + element.valueDrawer.
LineHeight().Round()).Add(offset))
}
}

217
elements/box.go Normal file
View File

@ -0,0 +1,217 @@
package elements
import "image"
import "tomo"
import "art"
import "art/shatter"
var boxCase = tomo.C("tomo", "box")
// Space is a list of spacing configurations that can be passed to some
// containers.
type Space int; const (
SpaceNone Space = 0
SpacePadding Space = 1
SpaceMargin Space = 2
SpaceBoth Space = SpacePadding | SpaceMargin
)
// Includes returns whether a spacing value has been or'd with another spacing
// value.
func (space Space) Includes (sub Space) bool {
return (space & sub) > 0
}
// Box is a container that lays out its children horizontally or vertically.
// Child elements can be set to contract to their minimum size, or expand to
// fill remaining space. Boxes can be nested and used together to create more
// complex layouts.
type Box struct {
container
padding bool
margin bool
vertical bool
}
// NewHBox creates a new horizontal box.
func NewHBox (space Space, children ...tomo.Element) (element *Box) {
element = &Box {
padding: space.Includes(SpacePadding),
margin: space.Includes(SpaceMargin),
}
element.entity = tomo.GetBackend().NewEntity(element)
element.minimumSize = element.updateMinimumSize
element.init()
element.Adopt(children...)
return
}
// NewHBox creates a new vertical box.
func NewVBox (space Space, children ...tomo.Element) (element *Box) {
element = &Box {
padding: space.Includes(SpacePadding),
margin: space.Includes(SpaceMargin),
vertical: true,
}
element.entity = tomo.GetBackend().NewEntity(element)
element.minimumSize = element.updateMinimumSize
element.init()
element.Adopt(children...)
return
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Box) Draw (destination art.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.entity.DrawBackground(art.Cut(destination, tile))
}
}
// Layout causes this element to perform a layout operation.
func (element *Box) Layout () {
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
bounds := element.entity.Bounds()
if element.padding { bounds = padding.Apply(bounds) }
var marginSize float64; if element.vertical {
marginSize = float64(margin.Y)
} else {
marginSize = float64(margin.X)
}
freeSpace, nExpanding := element.freeSpace()
expandingElementSize := freeSpace / nExpanding
// set the size and position of each element
x := float64(bounds.Min.X)
y := float64(bounds.Min.Y)
for index := 0; index < element.entity.CountChildren(); index ++ {
entry := element.scratch[element.entity.Child(index)]
var size float64; if entry.expand {
size = expandingElementSize
} else {
size = entry.minSize
}
var childBounds image.Rectangle; if element.vertical {
childBounds = tomo.Bounds(int(x), int(y), bounds.Dx(), int(size))
} else {
childBounds = tomo.Bounds(int(x), int(y), int(size), bounds.Dy())
}
element.entity.PlaceChild(index, childBounds)
if element.vertical {
y += size
if element.margin { y += marginSize }
} else {
x += size
if element.margin { x += marginSize }
}
}
}
// AdoptExpand adds one or more elements to the box. These elements will be
// expanded to fill in empty space.
func (element *Box) AdoptExpand (children ...tomo.Element) {
element.adopt(true, children...)
}
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Box) DrawBackground (destination art.Canvas) {
element.entity.DrawBackground(destination)
}
func (element *Box) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Box) freeSpace () (space float64, nExpanding float64) {
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
var marginSize int; if element.vertical {
marginSize = margin.Y
} else {
marginSize = margin.X
}
if element.vertical {
space = float64(element.entity.Bounds().Dy())
} else {
space = float64(element.entity.Bounds().Dx())
}
for _, entry := range element.scratch {
if entry.expand {
nExpanding ++;
} else {
space -= float64(entry.minSize)
}
}
if element.padding {
space -= float64(padding.Vertical())
}
if element.margin {
space -= float64(marginSize * (len(element.scratch) - 1))
}
return
}
func (element *Box) updateMinimumSize () {
margin := element.entity.Theme().Margin(tomo.PatternBackground, boxCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, boxCase)
var breadth, size int
var marginSize int; if element.vertical {
marginSize = margin.Y
} else {
marginSize = margin.X
}
for index := 0; index < element.entity.CountChildren(); index ++ {
childWidth, childHeight := element.entity.ChildMinimumSize(index)
var childBreadth, childSize int; if element.vertical {
childBreadth, childSize = childWidth, childHeight
} else {
childBreadth, childSize = childHeight, childWidth
}
key := element.entity.Child(index)
entry := element.scratch[key]
entry.minSize = float64(childSize)
element.scratch[key] = entry
if childBreadth > breadth {
breadth = childBreadth
}
size += childSize
if element.margin && index > 0 {
size += marginSize
}
}
var width, height int; if element.vertical {
width, height = breadth, size
} else {
width, height = size, breadth
}
if element.padding {
width += padding.Horizontal()
height += padding.Vertical()
}
element.entity.SetMinimumSize(width, height)
}

242
elements/button.go Normal file
View File

@ -0,0 +1,242 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
import "tomo/textdraw"
var buttonCase = tomo.C("tomo", "button")
// Button is a clickable button.
type Button struct {
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
pressed bool
text string
showText bool
hasIcon bool
iconId tomo.Icon
onClick func ()
}
// NewButton creates a new button with the specified label text.
func NewButton (text string) (element *Button) {
element = &Button { showText: true, enabled: true }
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
buttonCase))
element.SetText(text)
return
}
// Entity returns this element's entity.
func (element *Button) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Button) Draw (destination art.Canvas) {
state := element.state()
bounds := element.entity.Bounds()
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, buttonCase)
pattern.Draw(destination, bounds)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, buttonCase)
sink := element.entity.Theme().Sink(tomo.PatternButton, buttonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase)
offset := image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2).Add(bounds.Min)
if element.showText {
textBounds := element.drawer.LayoutBounds()
offset.X -= textBounds.Dx() / 2
offset.Y -= textBounds.Dy() / 2
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
}
if element.hasIcon {
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
if icon != nil {
iconBounds := icon.Bounds()
addedWidth := iconBounds.Dx()
iconOffset := offset
if element.showText {
addedWidth += margin.X
}
iconOffset.X -= addedWidth / 2
iconOffset.Y =
bounds.Min.Y +
(bounds.Dy() -
iconBounds.Dy()) / 2
if element.pressed {
iconOffset = iconOffset.Add(sink)
}
offset.X += addedWidth / 2
icon.Draw(destination, foreground, iconOffset)
}
}
if element.showText {
if element.pressed {
offset = offset.Add(sink)
}
element.drawer.Draw(destination, foreground, offset)
}
}
// OnClick sets the function to be called when the button is clicked.
func (element *Button) OnClick (callback func ()) {
element.onClick = callback
}
// Focus gives this element input focus.
func (element *Button) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this button is enabled or not.
func (element *Button) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this button can be clicked or not.
func (element *Button) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// SetText sets the button's label text.
func (element *Button) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
// current icon if it exists.
func (element *Button) SetIcon (id tomo.Icon) {
if id == tomo.IconNone {
element.hasIcon = false
} else {
if element.hasIcon && element.iconId == id { return }
element.hasIcon = true
element.iconId = id
}
element.updateMinimumSize()
element.entity.Invalidate()
}
// ShowText sets whether or not the button's text will be displayed.
func (element *Button) ShowText (showText bool) {
if element.showText == showText { return }
element.showText = showText
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Button) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
buttonCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Button) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *Button) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
element.Focus()
if button != input.ButtonLeft { return }
element.pressed = true
element.entity.Invalidate()
}
func (element *Button) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button != input.ButtonLeft { return }
element.pressed = false
within := position.In(element.entity.Bounds())
if element.Enabled() && within && element.onClick != nil {
element.onClick()
}
element.entity.Invalidate()
}
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.entity.Invalidate()
if !element.Enabled() { return }
if element.onClick != nil {
element.onClick()
}
}
}
func (element *Button) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternButton, buttonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, buttonCase)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
if element.hasIcon {
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, buttonCase)
if icon != nil {
bounds := icon.Bounds()
if element.showText {
minimumSize.Max.X += bounds.Dx()
minimumSize.Max.X += margin.X
} else {
minimumSize.Max.X = bounds.Dx()
}
}
}
minimumSize = padding.Inverse().Apply(minimumSize)
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
}
func (element *Button) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
}
}

156
elements/cell.go Normal file
View File

@ -0,0 +1,156 @@
package elements
import "tomo"
import "art"
import "art/artutil"
var cellCase = tomo.C("tomo", "cell")
// Cell is a single-element container that satisfies tomo.Selectable. It
// provides styling based on whether or not it is selected.
type Cell struct {
entity tomo.Entity
child tomo.Element
enabled bool
onSelectionChange func ()
}
// NewCell creates a new cell element. If padding is true, the cell will have
// padding on all sides. Child can be nil and added later with the Adopt()
// method.
func NewCell (child tomo.Element) (element *Cell) {
element = &Cell { enabled: true }
element.entity = tomo.GetBackend().NewEntity(element)
element.Adopt(child)
return
}
// Entity returns this element's entity.
func (element *Cell) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Cell) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
pattern := element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase)
if element.child == nil {
pattern.Draw(destination, bounds)
} else {
artutil.DrawShatter (
destination, pattern, bounds,
element.child.Entity().Bounds())
}
}
// Draw causes the element to perform a layout operation.
func (element *Cell) Layout () {
if element.child == nil { return }
bounds := element.entity.Bounds()
bounds = element.entity.Theme().Padding(tomo.PatternTableCell, cellCase).Apply(bounds)
element.entity.PlaceChild(0, bounds)
}
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Cell) DrawBackground (destination art.Canvas) {
element.entity.Theme().Pattern(tomo.PatternTableCell, element.state(), cellCase).
Draw(destination, element.entity.Bounds())
}
// Adopt sets this element's child. If nil is passed, any child is removed.
func (element *Cell) Adopt (child tomo.Element) {
if element.child != nil {
element.entity.Disown(element.entity.IndexOf(element.child))
}
if child != nil {
element.entity.Adopt(child)
}
element.child = child
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
element.entity.InvalidateLayout()
}
// Child returns this element's child. If there is no child, this method will
// return nil.
func (element *Cell) Child () tomo.Element {
return element.child
}
// Enabled returns whether this cell is enabled or not.
func (element *Cell) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this cell can be selected or not.
func (element *Cell) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
element.invalidateChild()
}
// OnSelectionChange sets a function to be called when this element is selected
// or unselected.
func (element *Cell) OnSelectionChange (callback func ()) {
element.onSelectionChange = callback
}
func (element *Cell) Selected () bool {
return element.entity.Selected()
}
func (element *Cell) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
element.entity.InvalidateLayout()
}
func (element *Cell) HandleSelectionChange () {
element.entity.Invalidate()
element.invalidateChild()
if element.onSelectionChange != nil {
element.onSelectionChange()
}
}
func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Cell) state () tomo.State {
return tomo.State {
Disabled: !element.enabled,
On: element.entity.Selected(),
}
}
func (element *Cell) updateMinimumSize () {
width, height := 0, 0
if element.child != nil {
childWidth, childHeight := element.entity.ChildMinimumSize(0)
width += childWidth
height += childHeight
}
padding := element.entity.Theme().Padding(tomo.PatternTableCell, cellCase)
width += padding.Horizontal()
height += padding.Vertical()
element.entity.SetMinimumSize(width, height)
}
func (element *Cell) invalidateChild () {
if element.child != nil {
element.child.Entity().Invalidate()
}
}

178
elements/checkbox.go Normal file
View File

@ -0,0 +1,178 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
import "tomo/textdraw"
var checkboxCase = tomo.C("tomo", "checkbox")
// Checkbox is a toggle-able checkbox with a label.
type Checkbox struct {
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
pressed bool
checked bool
text string
onToggle func ()
}
// NewCheckbox creates a new cbeckbox with the specified label text.
func NewCheckbox (text string, checked bool) (element *Checkbox) {
element = &Checkbox { checked: checked, enabled: true }
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
checkboxCase))
element.SetText(text)
return
}
// Entity returns this element's entity.
func (element *Checkbox) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Checkbox) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
state := tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
On: element.checked,
}
element.entity.DrawBackground(destination)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, checkboxCase)
pattern.Draw(destination, boxBounds)
textBounds := element.drawer.LayoutBounds()
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + margin.X,
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, checkboxCase)
element.drawer.Draw(destination, foreground, offset)
}
// OnToggle sets the function to be called when the checkbox is toggled.
func (element *Checkbox) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the checkbox is currently checked.
func (element *Checkbox) Value () (checked bool) {
return element.checked
}
// Focus gives this element input focus.
func (element *Checkbox) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this checkbox is enabled or not.
func (element *Checkbox) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this checkbox can be toggled or not.
func (element *Checkbox) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// SetText sets the checkbox's label text.
func (element *Checkbox) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Checkbox) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
checkboxCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Checkbox) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *Checkbox) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
element.entity.Invalidate()
}
func (element *Checkbox) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false
within := position.In(element.entity.Bounds())
if within {
element.checked = !element.checked
}
element.entity.Invalidate()
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
element.entity.Invalidate()
if element.onToggle != nil {
element.onToggle()
}
}
}
func (element *Checkbox) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
if element.text == "" {
element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
} else {
margin := element.entity.Theme().Margin(tomo.PatternBackground, checkboxCase)
element.entity.SetMinimumSize (
textBounds.Dy() + margin.X + textBounds.Dx(),
textBounds.Dy())
}
}

268
elements/combobox.go Normal file
View File

@ -0,0 +1,268 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
import "tomo/ability"
import "tomo/textdraw"
var comboBoxCase = tomo.C("tomo", "comboBox")
// Option specifies a ComboBox option. A blank option will display as "(None)".
type Option string
func (option Option) Title () string {
if option == "" {
return "(None)"
} else {
return string(option)
}
}
// ComboBox is an input that can be one of several predetermined values.
type ComboBox struct {
entity tomo.Entity
drawer textdraw.Drawer
options []Option
selected Option
enabled bool
pressed bool
onChange func ()
}
// NewComboBox creates a new ComboBox with the specifed options.
func NewComboBox (options ...Option) (element *ComboBox) {
if len(options) == 0 { options = []Option { "" } }
element = &ComboBox { enabled: true, options: options }
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
comboBoxCase))
element.Select(options[0])
return
}
// Entity returns this element's entity.
func (element *ComboBox) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ComboBox) Draw (destination art.Canvas) {
state := element.state()
bounds := element.entity.Bounds()
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, comboBoxCase)
pattern.Draw(destination, bounds)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, comboBoxCase)
sink := element.entity.Theme().Sink(tomo.PatternButton, comboBoxCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase)
padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase)
offset := image.Pt(0, bounds.Dy() / 2).Add(bounds.Min)
textBounds := element.drawer.LayoutBounds()
offset.Y -= textBounds.Dy() / 2
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
if icon != nil {
iconBounds := icon.Bounds()
addedWidth := iconBounds.Dx() + margin.X
iconOffset := bounds.Min
iconOffset.X += padding[3]
iconOffset.Y =
bounds.Min.Y +
(bounds.Dy() -
iconBounds.Dy()) / 2
if element.pressed {
iconOffset = iconOffset.Add(sink)
}
offset.X += addedWidth + padding[3]
icon.Draw(destination, foreground, iconOffset)
}
if element.pressed {
offset = offset.Add(sink)
}
element.drawer.Draw(destination, foreground, offset)
}
// OnChange sets the function to be called when this element's value is changed.
func (element *ComboBox) OnChange (callback func ()) {
element.onChange = callback
}
// Value returns this element's value.
func (element *ComboBox) Value () Option {
return element.selected
}
// Select sets this element's value.
func (element *ComboBox) Select (option Option) {
element.selected = option
element.drawer.SetText([]rune(option.Title()))
element.updateMinimumSize()
element.entity.Invalidate()
if element.onChange != nil {
element.onChange()
}
}
// Filled returns whether this element has a value other than (None).
func (element *ComboBox) Filled () bool {
return element.selected != ""
}
// Focus gives this element input focus.
func (element *ComboBox) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this element is enabled or not.
func (element *ComboBox) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this element is enabled or not.
func (element *ComboBox) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
func (element *ComboBox) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
comboBoxCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *ComboBox) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *ComboBox) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
element.Focus()
if button != input.ButtonLeft { return }
element.dropDown()
}
func (element *ComboBox) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) { }
func (element *ComboBox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
selectionDelta := 0
switch key {
case input.KeyEnter:
element.pressed = true
element.entity.Invalidate()
case input.KeyUp, input.KeyLeft:
selectionDelta = -1
case input.KeyDown, input.KeyRight:
selectionDelta = 1
}
if selectionDelta != 0 {
selected := 0
for index, option := range element.options {
if option == element.selected {
selected = index
}
}
selected += selectionDelta
if selected < 0 {
selected = len(element.options) - 1
} else if selected >= len(element.options) {
selected = 0
}
element.Select(element.options[selected])
}
}
func (element *ComboBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.entity.Invalidate()
if !element.Enabled() { return }
element.dropDown()
}
}
func (element *ComboBox) dropDown () {
window := element.entity.Window()
menu, err := window.NewMenu(element.entity.Bounds())
if err != nil { return }
cellToOption := make(map[ability.Selectable] Option)
list := NewList()
for _, option := range element.options {
option := option
cell := NewCell(NewLabel(option.Title()))
cellToOption[cell] = option
list.Adopt(cell)
if option == element.selected {
list.Select(cell)
}
}
list.OnClick(func () {
selected := list.Selected()
if selected == nil { return }
element.Select(cellToOption[selected])
menu.Close()
})
menu.Adopt(list)
list.Focus()
menu.Show()
}
func (element *ComboBox) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternButton, comboBoxCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, comboBoxCase)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
icon := element.entity.Theme().Icon(tomo.IconExpand, tomo.IconSizeSmall, comboBoxCase)
if icon != nil {
bounds := icon.Bounds()
minimumSize.Max.X += bounds.Dx()
minimumSize.Max.X += margin.X
}
minimumSize = padding.Inverse().Apply(minimumSize)
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
}
func (element *ComboBox) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
}
}

83
elements/container.go Normal file
View File

@ -0,0 +1,83 @@
package elements
import "tomo"
type scratchEntry struct {
expand bool
minSize float64
minBreadth float64
}
type container struct {
entity tomo.Entity
scratch map[tomo.Element] scratchEntry
minimumSize func ()
}
// Entity returns this element's entity.
func (container *container) Entity () tomo.Entity {
return container.entity
}
// Adopt adds one or more elements to the container.
func (container *container) Adopt (children ...tomo.Element) {
container.adopt(false, children...)
}
func (container *container) init () {
container.scratch = make(map[tomo.Element] scratchEntry)
}
func (container *container) adopt (expand bool, children ...tomo.Element) {
for _, child := range children {
container.entity.Adopt(child)
container.scratch[child] = scratchEntry { expand: expand }
}
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}
// Disown removes one or more elements from the container.
func (container *container) Disown (children ...tomo.Element) {
for _, child := range children {
index := container.entity.IndexOf(child)
if index < 0 { continue }
container.entity.Disown(index)
delete(container.scratch, child)
}
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}
// DisownAll removes all elements from the container.
func (container *container) DisownAll () {
func () {
for index := 0; index < container.entity.CountChildren(); index ++ {
index := index
defer container.entity.Disown(index)
}
} ()
container.scratch = make(map[tomo.Element] scratchEntry)
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}
// Child returns the child at the specified index.
func (container *container) Child (index int) tomo.Element {
if index < 0 || index >= container.entity.CountChildren() { return nil }
return container.entity.Child(index)
}
// CountChildren returns the amount of children in this container.
func (container *container) CountChildren () int {
return container.entity.CountChildren()
}
func (container *container) HandleChildMinimumSizeChange (child tomo.Element) {
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}

View File

@ -1,167 +0,0 @@
package core
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Core is a struct that implements some core functionality common to most
// widgets. It is meant to be embedded directly into a struct.
type Core struct {
canvas canvas.Canvas
metrics struct {
minimumWidth int
minimumHeight int
}
drawSizeChange func ()
onMinimumSizeChange func ()
onDamage func (region canvas.Canvas)
}
// NewCore creates a new element core and its corresponding control.
func NewCore (
drawSizeChange func (),
) (
core *Core,
control CoreControl,
) {
core = &Core {
drawSizeChange: drawSizeChange,
}
control = CoreControl { core: core }
return
}
// Bounds fulfills the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) Bounds () (bounds image.Rectangle) {
if core.canvas == nil { return }
return core.canvas.Bounds()
}
// MinimumSize fulfils the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) MinimumSize () (width, height int) {
return core.metrics.minimumWidth, core.metrics.minimumHeight
}
// DrawTo fulfills the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) DrawTo (canvas canvas.Canvas) {
core.canvas = canvas
if core.drawSizeChange != nil {
core.drawSizeChange()
}
}
// OnDamage fulfils the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) OnDamage (callback func (region canvas.Canvas)) {
core.onDamage = callback
}
// OnMinimumSizeChange fulfils the tomo.Element interface. This should not need
// to be overridden.
func (core *Core) OnMinimumSizeChange (callback func ()) {
core.onMinimumSizeChange = callback
}
// CoreControl is a struct that can exert control over a Core struct. It can be
// used as a canvas. It must not be directly embedded into an element, but
// instead kept as a private member. When a Core struct is created, a
// corresponding CoreControl struct is linked to it and returned alongside it.
type CoreControl struct {
core *Core
}
// ColorModel fulfills the draw.Image interface.
func (control CoreControl) ColorModel () (model color.Model) {
return color.RGBAModel
}
// At fulfills the draw.Image interface.
func (control CoreControl) At (x, y int) (pixel color.Color) {
if control.core.canvas == nil { return }
return control.core.canvas.At(x, y)
}
// Bounds fulfills the draw.Image interface.
func (control CoreControl) Bounds () (bounds image.Rectangle) {
if control.core.canvas == nil { return }
return control.core.canvas.Bounds()
}
// Set fulfills the draw.Image interface.
func (control CoreControl) Set (x, y int, c color.Color) () {
if control.core.canvas == nil { return }
control.core.canvas.Set(x, y, c)
}
// Buffer fulfills the canvas.Canvas interface.
func (control CoreControl) Buffer () (data []color.RGBA, stride int) {
if control.core.canvas == nil { return }
return control.core.canvas.Buffer()
}
// HasImage returns true if the core has an allocated image buffer, and false if
// it doesn't.
func (control CoreControl) HasImage () (has bool) {
return control.core.canvas != nil && !control.core.canvas.Bounds().Empty()
}
// DamageRegion pushes the selected region of pixels to the parent element. This
// does not need to be called when responding to a resize event.
func (control CoreControl) DamageRegion (regions ...image.Rectangle) {
if control.core.canvas == nil { return }
if control.core.onDamage != nil {
for _, region := range regions {
control.core.onDamage (
canvas.Cut(control.core.canvas, region))
}
}
}
// DamageAll pushes all pixels to the parent element. This does not need to be
// called when redrawing in response to a change in size.
func (control CoreControl) DamageAll () {
control.DamageRegion(control.core.Bounds())
}
// SetMinimumSize sets the minimum size of this element, notifying the parent
// element in the process.
func (control CoreControl) SetMinimumSize (width, height int) {
core := control.core
if width == core.metrics.minimumWidth &&
height == core.metrics.minimumHeight {
return
}
core.metrics.minimumWidth = width
core.metrics.minimumHeight = height
if control.core.onMinimumSizeChange != nil {
control.core.onMinimumSizeChange()
}
}
// ConstrainSize contstrains the specified width and height to the minimum width
// and height, and returns wether or not anything ended up being constrained.
func (control CoreControl) ConstrainSize (
inWidth, inHeight int,
) (
outWidth, outHeight int,
constrained bool,
) {
core := control.core
outWidth = inWidth
outHeight = inHeight
if outWidth < core.metrics.minimumWidth {
outWidth = core.metrics.minimumWidth
constrained = true
}
if outHeight < core.metrics.minimumHeight {
outHeight = core.metrics.minimumHeight
constrained = true
}
return
}

View File

@ -1,7 +0,0 @@
// Package core provides tools that allow elements to easily fulfill common
// interfaces without having to duplicate a ton of code. Each "core" is a type
// that can be embedded into an element directly, working to fulfill a
// particular interface. Each one comes with a corresponding core control, which
// provides an interface for elements to exert control over the core. Core
// controls should be kept private.
package core

View File

@ -1,118 +0,0 @@
package core
import "git.tebibyte.media/sashakoshka/tomo/input"
// FocusableCore is a struct that can be embedded into objects to make them
// focusable, giving them the default keynav behavior.
type FocusableCore struct {
focused bool
enabled bool
drawFocusChange func ()
onFocusRequest func () (granted bool)
onFocusMotionRequest func(input.KeynavDirection) (granted bool)
}
// NewFocusableCore creates a new focusability core and its corresponding
// control. If your element needs to visually update itself when it's focus
// state changes (which it should), a callback to draw and push the update can
// be specified.
func NewFocusableCore (
drawFocusChange func (),
) (
core *FocusableCore,
control FocusableCoreControl,
) {
core = &FocusableCore {
drawFocusChange: drawFocusChange,
enabled: true,
}
control = FocusableCoreControl { core: core }
return
}
// Focused returns whether or not this element is currently focused.
func (core *FocusableCore) Focused () (focused bool) {
return core.focused
}
// Focus focuses this element, if its parent element grants the request.
func (core *FocusableCore) Focus () {
if !core.enabled || core.focused { return }
if core.onFocusRequest != nil {
if core.onFocusRequest() {
core.focused = true
if core.drawFocusChange != nil {
core.drawFocusChange()
}
}
}
}
// HandleFocus causes this element to mark itself as focused, if it can
// currently be. Otherwise, it will return false and do nothing.
func (core *FocusableCore) HandleFocus (
direction input.KeynavDirection,
) (
accepted bool,
) {
direction = direction.Canon()
if !core.enabled { return false }
if core.focused && direction != input.KeynavDirectionNeutral {
return false
}
if core.focused == false {
core.focused = true
if core.drawFocusChange != nil { core.drawFocusChange() }
}
return true
}
// HandleUnfocus causes this element to mark itself as unfocused.
func (core *FocusableCore) HandleUnfocus () {
core.focused = false
if core.drawFocusChange != nil { core.drawFocusChange() }
}
// OnFocusRequest sets a function to be called when this element
// wants its parent element to focus it. Parent elements should return
// true if the request was granted, and false if it was not.
func (core *FocusableCore) OnFocusRequest (callback func () (granted bool)) {
core.onFocusRequest = callback
}
// OnFocusMotionRequest sets a function to be called when this
// element wants its parent element to focus the element behind or in
// front of it, depending on the specified direction. Parent elements
// should return true if the request was granted, and false if it was
// not.
func (core *FocusableCore) OnFocusMotionRequest (
callback func (direction input.KeynavDirection) (granted bool),
) {
core.onFocusMotionRequest = callback
}
// Enabled returns whether or not the element is enabled.
func (core *FocusableCore) Enabled () (enabled bool) {
return core.enabled
}
// FocusableCoreControl is a struct that can be used to exert control over a
// focusability core. It must not be directly embedded into an element, but
// instead kept as a private member. When a FocusableCore struct is created, a
// corresponding FocusableCoreControl struct is linked to it and returned
// alongside it.
type FocusableCoreControl struct {
core *FocusableCore
}
// SetEnabled sets whether the focusability core is enabled. If the state
// changes, this will call drawFocusChange.
func (control FocusableCoreControl) SetEnabled (enabled bool) {
if control.core.enabled == enabled { return }
control.core.enabled = enabled
if !enabled { control.core.focused = false }
if control.core.drawFocusChange != nil {
control.core.drawFocusChange()
}
}

322
elements/directory.go Normal file
View File

@ -0,0 +1,322 @@
package elements
import "image"
import "path/filepath"
import "tomo"
import "tomo/input"
import "art"
import "tomo/ability"
import "art/shatter"
// TODO: base on flow implementation of list. also be able to switch to a table
// variant for a more information dense view.
var directoryCase = tomo.C("tomo", "list")
type historyEntry struct {
location string
filesystem ReadDirStatFS
}
// Directory displays a list of files within a particular directory and
// file system.
type Directory struct {
container
entity tomo.Entity
scroll image.Point
contentBounds image.Rectangle
history []historyEntry
historyIndex int
onChoose func (file string)
onScrollBoundsChange func ()
}
// NewDirectory creates a new directory view. If within is nil, it will use
// the OS file system.
func NewDirectory (
location string,
within ReadDirStatFS,
) (
element *Directory,
err error,
) {
element = &Directory { }
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init()
err = element.SetLocation(location, within)
return
}
func (element *Directory) Draw (destination art.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.DrawBackground(art.Cut(destination, tile))
}
}
func (element *Directory) Layout () {
if element.scroll.Y > element.maxScrollHeight() {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.entity.Theme().Margin(tomo.PatternPinboard, directoryCase)
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
width := int(entry.minBreadth)
height := int(entry.minSize)
if width + dot.X > bounds.Max.X {
nextLine()
}
if typedChild, ok := child.(ability.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
rowHeight = height
}
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
dot.X += width + margin.X
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *Directory) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
element.selectNone()
}
func (element *Directory) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) { }
func (element *Directory) HandleChildMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element,
) {
element.selectNone()
if child, ok := child.(ability.Selectable); ok {
index := element.entity.IndexOf(child)
element.entity.SelectChild(index, true)
}
}
func (element *Directory) HandleChildMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element,
) { }
func (element *Directory) HandleChildFlexibleHeightChange (child ability.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// ScrollContentBounds returns the full content size of the element.
func (element *Directory) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *Directory) ScrollViewportBounds () image.Rectangle {
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *Directory) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *Directory) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *Directory) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *Directory) DrawBackground (destination art.Canvas) {
element.entity.Theme().Pattern(tomo.PatternPinboard, tomo.State { }, directoryCase).
Draw(destination, element.entity.Bounds())
}
func (element *Directory) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// Location returns the directory's location and filesystem.
func (element *Directory) Location () (string, ReadDirStatFS) {
if len(element.history) < 1 { return "", nil }
current := element.history[element.historyIndex]
return current.location, current.filesystem
}
// SetLocation sets the directory's location and filesystem. If within is nil,
// it will use the OS file system.
func (element *Directory) SetLocation (
location string,
within ReadDirStatFS,
) error {
if within == nil {
within = defaultFS { }
}
element.scroll = image.Point { }
if element.history != nil {
element.historyIndex ++
}
element.history = append (
element.history[:element.historyIndex],
historyEntry { location, within })
return element.Update()
}
// Backward goes back a directory in history
func (element *Directory) Backward () (bool, error) {
if element.historyIndex > 1 {
element.historyIndex --
return true, element.Update()
} else {
return false, nil
}
}
// Forward goes forward a directory in history
func (element *Directory) Forward () (bool, error) {
if element.historyIndex < len(element.history) - 1 {
element.historyIndex ++
return true, element.Update()
} else {
return false, nil
}
}
// Update refreshes the directory's contents.
func (element *Directory) Update () error {
location, filesystem := element.Location()
entries, err := filesystem.ReadDir(location)
children := make([]tomo.Element, len(entries))
for index, entry := range entries {
filePath := filepath.Join(location, entry.Name())
file, _ := NewFile(filePath, filesystem)
file.OnChoose (func () {
if element.onChoose != nil {
element.onChoose(filePath)
}
})
children[index] = file
}
element.DisownAll()
element.Adopt(children...)
return err
}
// OnChoose sets a function to be called when the user double-clicks a file or
// sub-directory within the directory view.
func (element *Directory) OnChoose (callback func (file string)) {
element.onChoose = callback
}
func (element *Directory) selectNone () {
for index := 0; index < element.entity.CountChildren(); index ++ {
element.entity.SelectChild(index, false)
}
}
func (element *Directory) maxScrollHeight () (height int) {
padding := element.entity.Theme().Padding(tomo.PatternSunken, directoryCase)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *Directory) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternPinboard, directoryCase)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)
if width > minimumWidth {
minimumWidth = width
}
key := element.entity.Child(index)
entry := element.scratch[key]
entry.minSize = float64(height)
entry.minBreadth = float64(width)
element.scratch[key] = entry
}
element.entity.SetMinimumSize (
minimumWidth + padding.Horizontal(),
padding.Vertical())
}

View File

@ -1,6 +1,3 @@
// Package elements provides several standard interfaces that elements can
// fulfill in order to inform other elements of their capabilities and what
// events they are able to process. Sub-packages of this package provide
// pre-made standard elements, as well as tools that can be used to easily
// create more.
// Package elements provides standard elements that are commonly used in GUI
// applications.
package elements

211
elements/document.go Normal file
View File

@ -0,0 +1,211 @@
package elements
import "image"
import "tomo"
import "art"
import "tomo/ability"
import "art/shatter"
var documentCase = tomo.C("tomo", "document")
// Document is a scrollable container capcable of laying out flexible child
// elements. Children can be added either inline (similar to an HTML/CSS inline
// element), or expanding (similar to an HTML/CSS block element).
type Document struct {
container
entity tomo.Entity
scroll image.Point
contentBounds image.Rectangle
onScrollBoundsChange func ()
}
// NewDocument creates a new document container.
func NewDocument (children ...tomo.Element) (element *Document) {
element = &Document { }
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init()
element.Adopt(children...)
return
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Document) Draw (destination art.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.entity.DrawBackground(art.Cut(destination, tile))
}
}
// Layout causes this element to perform a layout operation.
func (element *Document) Layout () {
if element.scroll.Y > element.maxScrollHeight() {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.entity.Theme().Margin(tomo.PatternBackground, documentCase)
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
if dot.X > xStart && entry.expand {
nextLine()
}
width := int(entry.minBreadth)
height := int(entry.minSize)
if width + dot.X > bounds.Max.X && !entry.expand {
nextLine()
}
if width < bounds.Dx() && entry.expand {
width = bounds.Dx()
}
if typedChild, ok := child.(ability.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
rowHeight = height
}
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
if entry.expand {
nextLine()
} else {
dot.X += width + margin.X
}
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// Adopt adds one or more elements to the container, placing each on its own
// line.
func (element *Document) Adopt (children ...tomo.Element) {
element.adopt(true, children...)
}
// AdoptInline adds one or more elements to the container, packing multiple
// elements onto the same line(s).
func (element *Document) AdoptInline (children ...tomo.Element) {
element.adopt(false, children...)
}
func (element *Document) HandleChildFlexibleHeightChange (child ability.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Document) DrawBackground (destination art.Canvas) {
element.entity.DrawBackground(destination)
}
func (element *Document) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// ScrollContentBounds returns the full content size of the element.
func (element *Document) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *Document) ScrollViewportBounds () image.Rectangle {
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *Document) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *Document) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *Document) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *Document) maxScrollHeight () (height int) {
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *Document) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternBackground, documentCase)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)
if width > minimumWidth {
minimumWidth = width
}
key := element.entity.Child(index)
entry := element.scratch[key]
entry.minSize = float64(height)
entry.minBreadth = float64(width)
element.scratch[key] = entry
}
element.entity.SetMinimumSize (
minimumWidth + padding.Horizontal(),
padding.Vertical())
}

View File

@ -1,193 +0,0 @@
package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/config"
// Element represents a basic on-screen object.
type Element interface {
// Bounds reports the element's bounding box. This must reflect the
// bounding box of the last canvas given to the element by DrawTo.
Bounds () (bounds image.Rectangle)
// DrawTo sets this element's canvas. This should only be called by the
// parent element. This is typically a region of the parent element's
// canvas.
DrawTo (canvas canvas.Canvas)
// OnDamage sets a function to be called when an area of the element is
// drawn on and should be pushed to the screen.
OnDamage (callback func (region canvas.Canvas))
// MinimumSize specifies the minimum amount of pixels this element's
// width and height may be set to. If the element is given a resize
// event with dimensions smaller than this, it will use its minimum
// instead of the offending dimension(s).
MinimumSize () (width, height int)
// OnMinimumSizeChange sets a function to be called when the element's
// minimum size is changed.
OnMinimumSizeChange (callback func ())
}
// Focusable represents an element that has keyboard navigation support. This
// includes inputs, buttons, sliders, etc. as well as any elements that have
// children (so keyboard navigation events can be propagated downward).
type Focusable interface {
Element
// Focused returns whether or not this element or any of its children
// are currently focused.
Focused () (selected bool)
// Focus focuses this element, if its parent element grants the
// request.
Focus ()
// HandleFocus causes this element to mark itself as focused. If the
// element does not have children, it is disabled, or there are no more
// selectable children in the given direction, it should return false
// and do nothing. Otherwise, it should select itself and any children
// (if applicable) and return true.
HandleFocus (direction input.KeynavDirection) (accepted bool)
// HandleDeselection causes this element to mark itself and all of its
// children as unfocused.
HandleUnfocus ()
// OnFocusRequest sets a function to be called when this element wants
// its parent element to focus it. Parent elements should return true if
// the request was granted, and false if it was not. If the parent
// element returns true, the element must act as if a HandleFocus call
// was made with KeynavDirectionNeutral.
OnFocusRequest (func () (granted bool))
// OnFocusMotionRequest sets a function to be called when this
// element wants its parent element to focus the element behind or in
// front of it, depending on the specified direction. Parent elements
// should return true if the request was granted, and false if it was
// not.
OnFocusMotionRequest (func (direction input.KeynavDirection) (granted bool))
}
// KeyboardTarget represents an element that can receive keyboard input.
type KeyboardTarget interface {
Element
// HandleKeyDown is called when a key is pressed down or repeated while
// this element has keyboard focus. It is important to note that not
// every key down event is guaranteed to be paired with exactly one key
// up event. This is the reason a list of modifier keys held down at the
// time of the key press is given.
HandleKeyDown (key input.Key, modifiers input.Modifiers)
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
HandleKeyUp (key input.Key, modifiers input.Modifiers)
}
// MouseTarget represents an element that can receive mouse events.
type MouseTarget interface {
Element
// Each of these handler methods is passed the position of the mouse
// cursor at the time of the event as x, y.
// HandleMouseDown is called when a mouse button is pressed down on this
// element.
HandleMouseDown (x, y int, button input.Button)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element.
HandleMouseUp (x, y int, button input.Button)
// HandleMouseMove is called when the mouse is moved over this element,
// or the mouse is moving while being held down and originally pressed
// down on this element.
HandleMouseMove (x, y int)
// HandleScroll is called when the mouse is scrolled. The X and Y
// direction of the scroll event are passed as deltaX and deltaY.
HandleMouseScroll (x, y int, deltaX, deltaY float64)
}
// Flexible represents an element who's preferred minimum height can change in
// response to its width.
type Flexible interface {
Element
// FlexibleHeightFor returns what the element's minimum height would be
// if resized to a specified width. This does not actually alter the
// state of the element in any way, but it may perform significant work,
// so it should be called sparingly.
//
// It is reccomended that parent containers check for this interface and
// take this method's value into account in order to support things like
// flow layouts and text wrapping, but it is not absolutely necessary.
// The element's MinimumSize method will still return the absolute
// minimum size that the element may be resized to.
//
// It is important to note that if a parent container checks for
// flexible chilren, it itself will likely need to be flexible.
FlexibleHeightFor (width int) (height int)
// OnFlexibleHeightChange sets a function to be called when the
// parameters affecting this element's flexible height are changed.
OnFlexibleHeightChange (callback func ())
}
// Scrollable represents an element that can be scrolled. It acts as a viewport
// through which its contents can be observed.
type Scrollable interface {
Element
// ScrollContentBounds returns the full content size of the element.
ScrollContentBounds () (bounds image.Rectangle)
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
ScrollViewportBounds () (bounds image.Rectangle)
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
ScrollTo (position image.Point)
// ScrollAxes returns the supported axes for scrolling.
ScrollAxes () (horizontal, vertical bool)
// OnScrollBoundsChange sets a function to be called when the element's
// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed.
OnScrollBoundsChange (callback func ())
}
// Collapsible represents an element who's minimum width and height can be
// manually resized. Scrollable elements should implement this if possible.
type Collapsible interface {
Element
// Collapse collapses the element's minimum width and height. A value of
// zero for either means that the element's normal value is used.
Collapse (width, height int)
}
// Themeable represents an element that can modify its appearance to fit within
// a theme.
type Themeable interface {
Element
// SetTheme sets the element's theme to something fulfilling the
// theme.Theme interface.
SetTheme (theme.Theme)
}
// Configurable represents an element that can modify its behavior to fit within
// a set of configuration parameters.
type Configurable interface {
Element
// SetConfig sets the element's configuration to something fulfilling
// the config.Config interface.
SetConfig (config.Config)
}

217
elements/file.go Normal file
View File

@ -0,0 +1,217 @@
package elements
import "time"
import "io/fs"
import "image"
import "tomo"
import "tomo/input"
import "art"
var fileCase = tomo.C("files", "file")
// File displays an interactive visual representation of a file within any
// file system.
type File struct {
entity tomo.Entity
lastClick time.Time
pressed bool
enabled bool
iconID tomo.Icon
filesystem fs.StatFS
location string
onChoose func ()
}
// NewFile creates a new file element. If within is nil, it will use the OS file
// system
func NewFile (
location string,
within fs.StatFS,
) (
element *File,
err error,
) {
element = &File { enabled: true }
element.entity = tomo.GetBackend().NewEntity(element)
err = element.SetLocation(location, within)
return
}
// Entity returns this element's entity.
func (element *File) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *File) Draw (destination art.Canvas) {
// background
state := element.state()
bounds := element.entity.Bounds()
sink := element.entity.Theme().Sink(tomo.PatternButton, fileCase)
element.entity.Theme().
Pattern(tomo.PatternButton, state, fileCase).
Draw(destination, bounds)
// icon
icon := element.icon()
if icon != nil {
iconBounds := icon.Bounds()
offset := image.Pt (
(bounds.Dx() - iconBounds.Dx()) / 2,
(bounds.Dy() - iconBounds.Dy()) / 2)
if element.pressed {
offset = offset.Add(sink)
}
icon.Draw (
destination,
element.entity.Theme().Color(tomo.ColorForeground, state, fileCase),
bounds.Min.Add(offset))
}
}
// Location returns the file's location and filesystem.
func (element *File) Location () (string, fs.StatFS) {
return element.location, element.filesystem
}
// SetLocation sets the file's location and filesystem. If within is nil, it
// will use the OS file system.
func (element *File) SetLocation (
location string,
within fs.StatFS,
) error {
if within == nil {
within = defaultFS { }
}
element.location = location
element.filesystem = within
return element.Update()
}
// Update refreshes the element to match the file it represents.
func (element *File) Update () error {
info, err := element.filesystem.Stat(element.location)
if err != nil {
element.iconID = tomo.IconError
} else if info.IsDir() {
element.iconID = tomo.IconDirectory
} else {
// TODO: choose icon based on file mime type
element.iconID = tomo.IconFile
}
element.updateMinimumSize()
element.entity.Invalidate()
return err
}
func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *File) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
if !element.Enabled() { return }
element.entity.Invalidate()
if element.onChoose != nil {
element.onChoose()
}
}
}
func (element *File) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *File) HandleSelectionChange () {
element.entity.Invalidate()
}
func (element *File) OnChoose (callback func ()) {
element.onChoose = callback
}
// Focus gives this element input focus.
func (element *File) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this file is enabled or not.
func (element *File) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this file is enabled or not.
func (element *File) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
func (element *File) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
if !element.entity.Focused() { element.Focus() }
if button != input.ButtonLeft { return }
element.pressed = true
element.entity.Invalidate()
}
func (element *File) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button != input.ButtonLeft { return }
element.pressed = false
within := position.In(element.entity.Bounds())
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
if element.Enabled() && within && element.onChoose != nil {
element.onChoose()
}
} else {
element.lastClick = time.Now()
}
element.entity.Invalidate()
}
func (element *File) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *File) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
On: element.entity.Selected(),
}
}
func (element *File) icon () art.Icon {
return element.entity.Theme().Icon(element.iconID, tomo.IconSizeLarge, fileCase)
}
func (element *File) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternButton, fileCase)
icon := element.icon()
if icon == nil {
element.entity.SetMinimumSize (
padding.Horizontal(),
padding.Vertical())
} else {
bounds := padding.Inverse().Apply(icon.Bounds())
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
}
}

25
elements/fs.go Normal file
View File

@ -0,0 +1,25 @@
package elements
import "os"
import "io/fs"
// ReadDirStatFS is a combination of fs.ReadDirFS and fs.StatFS. It is the
// minimum filesystem needed to satisfy a directory view.
type ReadDirStatFS interface {
fs.ReadDirFS
fs.StatFS
}
type defaultFS struct { }
func (defaultFS) Open (name string) (fs.File, error) {
return os.Open(name)
}
func (defaultFS) ReadDir (name string) ([]fs.DirEntry, error) {
return os.ReadDir(name)
}
func (defaultFS) Stat (name string) (fs.FileInfo, error) {
return os.Stat(name)
}

View File

@ -4,73 +4,48 @@ import "time"
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "tomo"
import "art"
import "art/shapes"
var clockCase = tomo.C("tomo", "clock")
// AnalogClock can display the time of day in an analog format.
type AnalogClock struct {
*core.Core
core core.CoreControl
time time.Time
config config.Wrapped
theme theme.Wrapped
entity tomo.Entity
time time.Time
}
// NewAnalogClock creates a new analog clock that displays the specified time.
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
element = &AnalogClock { }
element.theme.Case = theme.C("fun", "clock")
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(64, 64)
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(64, 64)
return
}
// SetTime changes the time that the clock displays.
func (element *AnalogClock) SetTime (newTime time.Time) {
if newTime == element.time { return }
element.time = newTime
element.redo()
// Entity returns this element's entity.
func (element *AnalogClock) Entity () tomo.Entity {
return element.entity
}
// SetTheme sets the element's theme.
func (element *AnalogClock) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.redo()
}
// Draw causes the element to draw to the specified destination canvas.
func (element *AnalogClock) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
// SetConfig sets the element's configuration.
func (element *AnalogClock) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.redo()
}
func (element *AnalogClock) redo () {
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *AnalogClock) draw () {
bounds := element.Bounds()
state := theme.State { }
pattern := element.theme.Pattern(theme.PatternSunken, state)
padding := element.theme.Padding(theme.PatternSunken)
pattern.Draw(element.core, bounds)
state := tomo.State { }
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, state, clockCase)
padding := element.entity.Theme().Padding(tomo.PatternSunken, clockCase)
pattern.Draw(destination, bounds)
bounds = padding.Apply(bounds)
foreground := element.theme.Color(theme.ColorForeground, state)
accent := element.theme.Color(theme.ColorAccent, state)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, clockCase)
accent := element.entity.Theme().Color(tomo.ColorAccent, state, clockCase)
for hour := 0; hour < 12; hour ++ {
element.radialLine (
destination,
foreground,
0.8, 0.9, float64(hour) / 6 * math.Pi)
}
@ -79,34 +54,37 @@ func (element *AnalogClock) draw () {
minute := float64(element.time.Minute()) + second / 60
hour := float64(element.time.Hour()) + minute / 60
element.radialLine(foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
element.radialLine(foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
element.radialLine(accent, 0, 0.7, (second - 15) / 30 * math.Pi)
element.radialLine(destination, foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
element.radialLine(destination, foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
element.radialLine(destination, accent, 0, 0.7, (second - 15) / 30 * math.Pi)
}
// FlexibleHeightFor constrains the clock's minimum size to a 1:1 aspect ratio.
func (element *AnalogClock) FlexibleHeightFor (width int) (height int) {
return width
// SetTime changes the time that the clock displays.
func (element *AnalogClock) SetTime (newTime time.Time) {
if newTime == element.time { return }
element.time = newTime
element.entity.Invalidate()
}
// OnFlexibleHeightChange sets a function to be called when the parameters
// affecting the clock's flexible height change.
func (element *AnalogClock) OnFlexibleHeightChange (func ()) { }
func (element *AnalogClock) HandleThemeChange () {
element.entity.Invalidate()
}
func (element *AnalogClock) radialLine (
destination art.Canvas,
source color.RGBA,
inner float64,
outer float64,
radian float64,
) {
bounds := element.Bounds()
bounds := element.entity.Bounds()
width := float64(bounds.Dx()) / 2
height := float64(bounds.Dy()) / 2
min := element.Bounds().Min.Add(image.Pt (
min := bounds.Min.Add(image.Pt (
int(math.Cos(radian) * inner * width + width),
int(math.Sin(radian) * inner * height + height)))
max := element.Bounds().Min.Add(image.Pt (
max := bounds.Min.Add(image.Pt (
int(math.Cos(radian) * outer * width + width),
int(math.Sin(radian) * outer * height + height)))
shapes.ColorLine(element.core, source, 1, min, max)
shapes.ColorLine(destination, source, 1, min, max)
}

View File

@ -1,12 +1,15 @@
package fun
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
import "tomo"
import "tomo/input"
import "art"
import "art/artutil"
import "tomo/elements/fun/music"
var pianoCase = tomo.C("tomo", "piano")
var flatCase = tomo.C("tomo", "piano", "flatKey")
var sharpCase = tomo.C("tomo", "piano", "sharpKey")
const pianoKeyWidth = 18
@ -17,19 +20,14 @@ type pianoKey struct {
// Piano is an element that can be used to input midi notes.
type Piano struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
low, high music.Octave
config config.Wrapped
theme theme.Wrapped
entity tomo.Entity
low, high music.Octave
flatKeys []pianoKey
sharpKeys []pianoKey
contentBounds image.Rectangle
enabled bool
pressed *pianoKey
keynavPressed map[music.Note] bool
@ -40,11 +38,7 @@ type Piano struct {
// NewPiano returns a new piano element with a lowest and highest octave,
// inclusive. If low is greater than high, they will be swapped.
func NewPiano (low, high music.Octave) (element *Piano) {
if low > high {
temp := low
low = high
high = temp
}
if low > high { low, high = high, low }
element = &Piano {
low: low,
@ -52,17 +46,68 @@ func NewPiano (low, high music.Octave) (element *Piano) {
keynavPressed: make(map[music.Note] bool),
}
element.theme.Case = theme.C("fun", "piano")
element.Core, element.core = core.NewCore (func () {
element.recalculate()
element.draw()
})
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.redo)
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *Piano) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Piano) Draw (destination art.Canvas) {
element.recalculate()
state := tomo.State {
Focused: element.entity.Focused(),
Disabled: !element.Enabled(),
}
for _, key := range element.flatKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawFlat (
destination,
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
for _, key := range element.sharpKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawSharp (
destination,
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
pattern := element.entity.Theme().Pattern(tomo.PatternPinboard, state, pianoCase)
artutil.DrawShatter (
destination, pattern, element.entity.Bounds(),
element.contentBounds)
}
// Focus gives this element input focus.
func (element *Piano) Focus () {
element.entity.Focus()
}
// Enabled returns whether this piano can be played or not.
func (element *Piano) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this piano can be played or not.
func (element *Piano) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// OnPress sets a function to be called when a key is pressed.
func (element *Piano) OnPress (callback func (note music.Note)) {
element.onPress = callback
@ -85,16 +130,14 @@ func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
element.onRelease((*element.pressed).Note)
}
element.pressed = nil
element.redo()
element.entity.Invalidate()
}
func (element *Piano) HandleMouseMove (x, y int) {
func (element *Piano) HandleMotion (x, y int) {
if element.pressed == nil { return }
element.pressUnderMouseCursor(image.Pt(x, y))
}
func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Piano) pressUnderMouseCursor (point image.Point) {
// find out which note is being pressed
newKey := (*pianoKey)(nil)
@ -123,7 +166,7 @@ func (element *Piano) pressUnderMouseCursor (point image.Point) {
if element.onPress != nil {
element.onPress((*element.pressed).Note)
}
element.redo()
element.entity.Invalidate()
}
}
@ -183,7 +226,7 @@ func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if element.onPress != nil {
element.onPress(note)
}
element.redo()
element.entity.Invalidate()
}
}
@ -196,30 +239,17 @@ func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if element.onRelease != nil {
element.onRelease(note)
}
element.redo()
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Piano) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
func (element *Piano) HandleThemeChange () {
element.updateMinimumSize()
element.recalculate()
element.redo()
}
// SetConfig sets the element's configuration.
func (element *Piano) SetConfig (new config.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.recalculate()
element.redo()
element.entity.Invalidate()
}
func (element *Piano) updateMinimumSize () {
padding := element.theme.Padding(theme.PatternPinboard)
element.core.SetMinimumSize (
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
element.entity.SetMinimumSize (
pianoKeyWidth * 7 * element.countOctaves() +
padding.Horizontal(),
64 + padding.Vertical())
@ -237,19 +267,12 @@ func (element *Piano) countSharps () int {
return element.countOctaves() * 5
}
func (element *Piano) redo () {
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Piano) recalculate () {
element.flatKeys = make([]pianoKey, element.countFlats())
element.sharpKeys = make([]pianoKey, element.countSharps())
padding := element.theme.Padding(theme.PatternPinboard)
bounds := padding.Apply(element.Bounds())
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
bounds := padding.Apply(element.entity.Bounds())
dot := bounds.Min
note := element.low.Note(0)
@ -280,52 +303,24 @@ func (element *Piano) recalculate () {
}
}
func (element *Piano) draw () {
state := theme.State {
Focused: element.Focused(),
Disabled: !element.Enabled(),
}
for _, key := range element.flatKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawFlat (
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
for _, key := range element.sharpKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawSharp (
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
pattern := element.theme.Pattern(theme.PatternPinboard, state)
artist.DrawShatter (
element.core, pattern, element.contentBounds)
}
func (element *Piano) drawFlat (
destination art.Canvas,
bounds image.Rectangle,
pressed bool,
state theme.State,
state tomo.State,
) {
state.Pressed = pressed
pattern := element.theme.Theme.Pattern (
theme.PatternButton, state, theme.C("fun", "flatKey"))
artist.DrawBounds(element.core, pattern, bounds)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, flatCase)
pattern.Draw(destination, bounds)
}
func (element *Piano) drawSharp (
destination art.Canvas,
bounds image.Rectangle,
pressed bool,
state theme.State,
state tomo.State,
) {
state.Pressed = pressed
pattern := element.theme.Theme.Pattern (
theme.PatternButton, state, theme.C("fun", "sharpKey"))
artist.DrawBounds(element.core, pattern, bounds)
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, sharpCase)
pattern.Draw(destination, bounds)
}

80
elements/icon.go Normal file
View File

@ -0,0 +1,80 @@
package elements
import "image"
import "tomo"
import "art"
var iconCase = tomo.C("tomo", "icon")
// Icon is an element capable of displaying a singular icon.
type Icon struct {
entity tomo.Entity
id tomo.Icon
size tomo.IconSize
}
// Icon creates a new icon element.
func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) {
element = &Icon {
id: id,
size: size,
}
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *Icon) Entity () tomo.Entity {
return element.entity
}
// SetIcon sets the element's icon.
func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) {
element.id = id
element.size = size
if element.entity == nil { return }
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Icon) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Icon) Draw (destination art.Canvas) {
if element.entity == nil { return }
bounds := element.entity.Bounds()
state := tomo.State { }
element.entity.Theme().
Pattern(tomo.PatternBackground, state, iconCase).
Draw(destination, bounds)
icon := element.icon()
if icon != nil {
iconBounds := icon.Bounds()
offset := image.Pt (
(bounds.Dx() - iconBounds.Dx()) / 2,
(bounds.Dy() - iconBounds.Dy()) / 2)
icon.Draw (
destination,
element.entity.Theme().Color(tomo.ColorForeground, state, iconCase),
bounds.Min.Add(offset))
}
}
func (element *Icon) icon () art.Icon {
return element.entity.Theme().Icon(element.id, element.size, iconCase)
}
func (element *Icon) updateMinimumSize () {
icon := element.icon()
if icon == nil {
element.entity.SetMinimumSize(0, 0)
} else {
bounds := icon.Bounds()
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
}
}

35
elements/image.go Normal file
View File

@ -0,0 +1,35 @@
package elements
import "image"
import "tomo"
import "art"
import "art/patterns"
// TODO: this element is lame need to make it better
// Image is an element capable of displaying an image.
type Image struct {
entity tomo.Entity
buffer art.Canvas
}
// NewImage creates a new image element.
func NewImage (image image.Image) (element *Image) {
element = &Image { buffer: art.FromImage(image) }
element.entity = tomo.GetBackend().NewEntity(element)
bounds := element.buffer.Bounds()
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
return
}
// Entity returns this element's entity.
func (element *Image) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Image) Draw (destination art.Canvas) {
if element.entity == nil { return }
(patterns.Texture { Canvas: element.buffer }).
Draw(destination, element.entity.Bounds())
}

206
elements/label.go Normal file
View File

@ -0,0 +1,206 @@
package elements
import "image"
import "golang.org/x/image/math/fixed"
import "tomo"
import "tomo/data"
import "tomo/input"
import "art"
import "tomo/textdraw"
var labelCase = tomo.C("tomo", "label")
// Label is a simple text box.
type Label struct {
entity tomo.Entity
align textdraw.Align
wrap bool
text string
drawer textdraw.Drawer
forcedColumns int
forcedRows int
minHeight int
}
// NewLabel creates a new label.
func NewLabel (text string) (element *Label) {
element = &Label { }
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal, labelCase))
element.SetText(text)
return
}
// NewLabelWrapped creates a new label with text wrapping on.
func NewLabelWrapped (text string) (element *Label) {
element = NewLabel(text)
element.SetWrap(true)
return
}
// Entity returns this element's entity.
func (element *Label) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Label) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
if element.wrap {
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.entity.DrawBackground(destination)
textBounds := element.drawer.LayoutBounds()
foreground := element.entity.Theme().Color (
tomo.ColorForeground,
tomo.State { }, labelCase)
element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min))
}
// Copy copies the label's textto the clipboard.
func (element *Label) Copy () {
window := element.entity.Window()
if window != nil {
window.Copy(data.Bytes(data.MimePlain, []byte(element.text)))
}
}
// EmCollapse forces a minimum width and height upon the label. The width is
// measured in emspaces, and the height is measured in lines. If a zero value is
// given for a dimension, its minimum will be determined by the label's content.
// If the label's content is greater than these dimensions, it will be truncated
// to fit.
func (element *Label) EmCollapse (columns int, rows int) {
element.forcedColumns = columns
element.forcedRows = rows
element.updateMinimumSize()
}
// FlexibleHeightFor returns the reccomended height for this element based on
// the given width in order to allow the text to wrap properly.
func (element *Label) FlexibleHeightFor (width int) (height int) {
if element.wrap {
return element.drawer.ReccomendedHeightFor(width)
} else {
return element.minHeight
}
}
// SetText sets the label's text.
func (element *Label) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetWrap sets wether or not the label's text wraps. If the text is set to
// wrap, the element will have a minimum size of a single character and
// automatically wrap its text. If the text is set to not wrap, the element will
// have a minimum size that fits its text.
func (element *Label) SetWrap (wrap bool) {
if wrap == element.wrap { return }
if !wrap {
element.drawer.SetMaxWidth(0)
element.drawer.SetMaxHeight(0)
}
element.wrap = wrap
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetAlign sets the alignment method of the label.
func (element *Label) SetAlign (align textdraw.Align) {
if align == element.align { return }
element.align = align
element.drawer.SetAlign(align)
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Label) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal, labelCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Label) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button == input.ButtonRight {
element.contextMenu(position)
}
}
func (element *Label) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) { }
func (element *Label) contextMenu (position image.Point) {
window := element.entity.Window()
menu, err := window.NewMenu(image.Rectangle { position, position })
if err != nil { return }
closeAnd := func (callback func ()) func () {
return func () { callback(); menu.Close() }
}
copyButton := NewButton("Copy")
copyButton.ShowText(false)
copyButton.SetIcon(tomo.IconCopy)
copyButton.OnClick(closeAnd(element.Copy))
menu.Adopt (NewHBox (
SpaceNone,
copyButton,
))
copyButton.Focus()
menu.Show()
}
func (element *Label) updateMinimumSize () {
var width, height int
if element.wrap {
em := element.drawer.Em().Round()
if em < 1 {
em = element.entity.Theme().Padding(tomo.PatternBackground, labelCase)[0]
}
width, height = em, element.drawer.LineHeight().Round()
element.entity.NotifyFlexibleHeightChange()
} else {
bounds := element.drawer.LayoutBounds()
width, height = bounds.Dx(), bounds.Dy()
}
if element.forcedColumns > 0 {
width =
element.drawer.Em().
Mul(fixed.I(element.forcedColumns)).Floor()
}
if element.forcedRows > 0 {
height =
element.drawer.LineHeight().
Mul(fixed.I(element.forcedRows)).Floor()
}
element.minHeight = height
element.entity.SetMinimumSize(width, height)
}

View File

@ -1,4 +1,6 @@
package basicElements
package elements
import "tomo"
// Numeric is a type constraint representing a number.
type Numeric interface {
@ -10,24 +12,29 @@ type Numeric interface {
// LerpSlider is a slider that has a minimum and maximum value, and who's value
// can be any numeric type.
type LerpSlider[T Numeric] struct {
*Slider
slider
min T
max T
}
// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. If
// vertical is set to true, the slider will be vertical instead of horizontal.
func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) {
if min > max {
temp := max
max = min
min = temp
}
// NewVLerpSlider creates a new horizontal LerpSlider with a minimum and maximum
// value.
func NewVLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) {
element = NewHLerpSlider(min, max, value)
element.vertical = true
return
}
// NewHLerpSlider creates a new horizontal LerpSlider with a minimum and maximum
// value.
func NewHLerpSlider[T Numeric] (min, max T, value T) (element *LerpSlider[T]) {
if min > max { min, max = max, min }
element = &LerpSlider[T] {
Slider: NewSlider(0, vertical),
min: min,
max: max,
}
element.entity = tomo.GetBackend().NewEntity(element)
element.construct()
element.SetValue(value)
return
}
@ -35,13 +42,13 @@ func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *Ler
// SetValue sets the slider's value.
func (element *LerpSlider[T]) SetValue (value T) {
value -= element.min
element.Slider.SetValue(float64(value) / float64(element.Range()))
element.slider.SetValue(float64(value) / float64(element.Range()))
}
// Value returns the slider's value.
func (element *LerpSlider[T]) Value () (value T) {
return T (
float64(element.Slider.Value()) * float64(element.Range())) +
float64(element.slider.Value()) * float64(element.Range())) +
element.min
}

448
elements/list.go Normal file
View File

@ -0,0 +1,448 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
import "tomo/ability"
import "art/artutil"
type list struct {
container
entity tomo.Entity
c tomo.Case
enabled bool
scroll image.Point
contentBounds image.Rectangle
selected int
forcedMinimumWidth int
forcedMinimumHeight int
onClick func ()
onSelectionChange func ()
onScrollBoundsChange func ()
}
type List struct {
list
}
type FlowList struct {
list
}
func NewList (children ...tomo.Element) (element *List) {
element = &List { }
element.c = tomo.C("tomo", "list")
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init(children...)
return
}
func NewFlowList (children ...tomo.Element) (element *FlowList) {
element = &FlowList { }
element.c = tomo.C("tomo", "flowList")
element.entity = tomo.GetBackend().NewEntity(element)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init(children...)
return
}
func (element *list) init (children ...tomo.Element) {
element.selected = -1
element.enabled = true
element.container.init()
element.Adopt(children...)
}
func (element *list) Draw (destination art.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, element.state(), element.c)
artutil.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...)
}
func (element *List) Layout () {
if element.scroll.Y > element.maxScrollHeight() {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
width := bounds.Dx()
height := int(entry.minSize)
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
dot.Y += height
dot.Y += margin.Y
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *FlowList) Layout () {
if element.scroll.Y > element.maxScrollHeight() {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
width := int(entry.minBreadth)
height := int(entry.minSize)
if width + dot.X > bounds.Max.X {
nextLine()
}
if typedChild, ok := child.(ability.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
rowHeight = height
}
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
dot.X += width + margin.X
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *list) Selected () ability.Selectable {
if element.selected == -1 { return nil }
child, ok := element.entity.Child(element.selected).(ability.Selectable)
if !ok { return nil }
return child
}
func (element *list) Select (child ability.Selectable) {
index := element.entity.IndexOf(child)
if element.selected == index { return }
element.selectNone()
element.selected = index
element.entity.SelectChild(index, true)
if element.onSelectionChange != nil {
element.onSelectionChange()
}
element.scrollToSelected()
}
func (element *list) Enabled () bool {
return element.enabled
}
func (element *list) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
func (element *list) Focus () {
element.entity.Focus()
}
func (element *list) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *list) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.enabled { return }
element.Focus()
element.selectNone()
}
func (element *list) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) { }
func (element *list) HandleChildMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element,
) {
if !element.enabled { return }
element.Focus()
if child, ok := child.(ability.Selectable); ok {
element.Select(child)
}
}
func (element *list) HandleChildMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
child tomo.Element,
) {
if !position.In(child.Entity().Bounds()) { return }
if element.onClick != nil {
element.onClick()
}
}
func (element *list) HandleChildFlexibleHeightChange (child ability.Flexible) {
element.minimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *list) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
index := -1
switch key {
case input.KeyUp, input.KeyLeft:
index = element.selected - 1
case input.KeyDown, input.KeyRight:
index = element.selected + 1
case input.KeyEnter:
if element.onClick != nil {
element.onClick()
}
}
if index >= 0 && index < element.entity.CountChildren() {
element.selectNone()
element.selected = index
element.entity.SelectChild(index, true)
if element.onSelectionChange != nil {
element.onSelectionChange()
}
element.scrollToSelected()
}
}
func (element *list) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
func (element *list) DrawBackground (destination art.Canvas) {
element.entity.DrawBackground(destination)
}
func (element *list) HandleThemeChange () {
element.minimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// Collapse forces a minimum width and height upon the list. If a zero value is
// given for a dimension, its minimum will be determined by the list's content.
// If the list's height goes beyond the forced size, it will need to be accessed
// via scrolling. If an entry's width goes beyond the forced size, its text will
// be truncated so that it fits.
func (element *list) Collapse (width, height int) {
if
element.forcedMinimumWidth == width &&
element.forcedMinimumHeight == height {
return
}
element.forcedMinimumWidth = width
element.forcedMinimumHeight = height
element.minimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// ScrollContentBounds returns the full content size of the element.
func (element *list) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *list) ScrollViewportBounds () image.Rectangle {
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *list) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *list) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
func (element *list) OnClick (callback func ()) {
element.onClick = callback
}
func (element *list) OnSelectionChange (callback func ()) {
element.onSelectionChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *list) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *list) selectNone () {
if element.selected >= 0 {
element.entity.SelectChild(element.selected, false)
if element.onSelectionChange != nil {
element.onSelectionChange()
}
}
}
func (element *list) scrollToSelected () {
if element.selected < 0 { return }
target := element.entity.Child(element.selected).Entity().Bounds()
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
bounds := padding.Apply(element.entity.Bounds())
if target.Min.Y < bounds.Min.Y {
element.scroll.Y -= bounds.Min.Y - target.Min.Y
element.entity.Invalidate()
element.entity.InvalidateLayout()
} else if target.Max.Y > bounds.Max.Y {
element.scroll.Y += target.Max.Y - bounds.Max.Y
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
}
func (element *list) state () tomo.State {
return tomo.State {
Focused: element.entity.Focused(),
Disabled: !element.enabled,
}
}
func (element *list) maxScrollHeight () (height int) {
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *List) updateMinimumSize () {
margin := element.entity.Theme().Margin(tomo.PatternSunken, element.c)
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
width := 0
height := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
if index > 0 { height += margin.Y }
child := element.entity.Child(index)
entry := element.scratch[child]
entryWidth, entryHeight := element.entity.ChildMinimumSize(index)
entry.minBreadth = float64(entryWidth)
entry.minSize = float64(entryHeight)
element.scratch[child] = entry
height += entryHeight
if width < entryWidth { width = entryWidth }
}
width += padding.Horizontal()
height += padding.Vertical()
if element.forcedMinimumWidth > 0 {
width = element.forcedMinimumWidth
}
if element.forcedMinimumHeight > 0 {
height = element.forcedMinimumHeight
}
element.entity.SetMinimumSize(width, height)
}
func (element *FlowList) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternSunken, element.c)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)
if width > minimumWidth {
minimumWidth = width
}
key := element.entity.Child(index)
entry := element.scratch[key]
entry.minSize = float64(height)
entry.minBreadth = float64(width)
element.scratch[key] = entry
}
element.entity.SetMinimumSize (
minimumWidth + padding.Horizontal(),
padding.Vertical())
}

67
elements/progressbar.go Normal file
View File

@ -0,0 +1,67 @@
package elements
import "image"
import "tomo"
import "art"
var progressBarCase = tomo.C("tomo", "progressBar")
// ProgressBar displays a visual indication of how far along a task is.
type ProgressBar struct {
entity tomo.Entity
progress float64
}
// NewProgressBar creates a new progress bar displaying the given progress
// level.
func NewProgressBar (progress float64) (element *ProgressBar) {
if progress < 0 { progress = 0 }
if progress > 1 { progress = 1 }
element = &ProgressBar { progress: progress }
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *ProgressBar) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ProgressBar) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
pattern := element.entity.Theme().Pattern(tomo.PatternSunken, tomo.State { }, progressBarCase)
padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase)
pattern.Draw(destination, bounds)
bounds = padding.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
mercury := element.entity.Theme().Pattern(tomo.PatternMercury, tomo.State { }, progressBarCase)
mercury.Draw(destination, meterBounds)
}
// SetProgress sets the progress level of the bar.
func (element *ProgressBar) SetProgress (progress float64) {
if progress < 0 { progress = 0 }
if progress > 1 { progress = 1 }
if progress == element.progress { return }
element.progress = progress
element.entity.Invalidate()
}
func (element *ProgressBar) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *ProgressBar) updateMinimumSize() {
padding := element.entity.Theme().Padding(tomo.PatternSunken, progressBarCase)
innerPadding := element.entity.Theme().Padding(tomo.PatternMercury, progressBarCase)
element.entity.SetMinimumSize (
padding.Horizontal() + innerPadding.Horizontal(),
padding.Vertical() + innerPadding.Vertical())
}

240
elements/scroll.go Normal file
View File

@ -0,0 +1,240 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
import "tomo/ability"
var scrollCase = tomo.C("tomo", "scroll")
// ScrollMode specifies which sides of a Scroll have scroll bars.
type ScrollMode int; const (
ScrollNeither ScrollMode = 0
ScrollVertical ScrollMode = 1
ScrollHorizontal ScrollMode = 2
ScrollBoth ScrollMode = ScrollVertical | ScrollHorizontal
)
// Includes returns whether a scroll mode has been or'd with another scroll
// mode.
func (mode ScrollMode) Includes (sub ScrollMode) bool {
return (mode & sub) > 0
}
// Scroll adds scroll bars to any scrollable element. It also captures scroll
// wheel input.
type Scroll struct {
entity tomo.Entity
child ability.Scrollable
horizontal *ScrollBar
vertical *ScrollBar
}
// NewScroll creates a new scroll element.
func NewScroll (mode ScrollMode, child ability.Scrollable) (element *Scroll) {
element = &Scroll { }
element.entity = tomo.GetBackend().NewEntity(element)
if mode.Includes(ScrollHorizontal) {
element.horizontal = NewHScrollBar()
element.horizontal.OnScroll (func (viewport image.Point) {
if element.child != nil {
element.child.ScrollTo(viewport)
}
if element.vertical != nil {
element.vertical.SetBounds (
element.child.ScrollContentBounds(),
element.child.ScrollViewportBounds())
}
})
element.entity.Adopt(element.horizontal)
}
if mode.Includes(ScrollVertical) {
element.vertical = NewVScrollBar()
element.vertical.OnScroll (func (viewport image.Point) {
if element.child != nil {
element.child.ScrollTo(viewport)
}
if element.horizontal != nil {
element.horizontal.SetBounds (
element.child.ScrollContentBounds(),
element.child.ScrollViewportBounds())
}
})
element.entity.Adopt(element.vertical)
}
element.Adopt(child)
return
}
// Entity returns this element's entity.
func (element *Scroll) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Scroll) Draw (destination art.Canvas) {
if element.horizontal != nil && element.vertical != nil {
bounds := element.entity.Bounds()
bounds.Min = image.Pt (
bounds.Max.X - element.vertical.Entity().Bounds().Dx(),
bounds.Max.Y - element.horizontal.Entity().Bounds().Dy())
state := tomo.State { }
deadArea := element.entity.Theme().Pattern(tomo.PatternDead, state, scrollCase)
deadArea.Draw(art.Cut(destination, bounds), bounds)
}
}
// Layout causes this element to perform a layout operation.
func (element *Scroll) Layout () {
bounds := element.entity.Bounds()
child := bounds
iHorizontal := element.entity.IndexOf(element.horizontal)
iVertical := element.entity.IndexOf(element.vertical)
iChild := element.entity.IndexOf(element.child)
var horizontal, vertical image.Rectangle
if element.horizontal != nil {
_, hMinHeight := element.entity.ChildMinimumSize(iHorizontal)
child.Max.Y -= hMinHeight
}
if element.vertical != nil {
vMinWidth, _ := element.entity.ChildMinimumSize(iVertical)
child.Max.X -= vMinWidth
}
horizontal.Min.X = bounds.Min.X
horizontal.Max.X = child.Max.X
horizontal.Min.Y = child.Max.Y
horizontal.Max.Y = bounds.Max.Y
vertical.Min.X = child.Max.X
vertical.Max.X = bounds.Max.X
vertical.Min.Y = bounds.Min.Y
vertical.Max.Y = child.Max.Y
if element.horizontal != nil {
element.entity.PlaceChild (iHorizontal, horizontal)
}
if element.vertical != nil {
element.entity.PlaceChild(iVertical, vertical)
}
if element.child != nil {
element.entity.PlaceChild(iChild, child)
}
}
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Scroll) DrawBackground (destination art.Canvas) {
element.entity.DrawBackground(destination)
}
// Adopt sets this element's child. If nil is passed, any child is removed.
func (element *Scroll) Adopt (child ability.Scrollable) {
if element.child != nil {
element.entity.Disown(element.entity.IndexOf(element.child))
}
if child != nil {
element.entity.Adopt(child)
}
element.child = child
element.updateEnabled()
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// Child returns this element's child. If there is no child, this method will
// return nil.
func (element *Scroll) Child () ability.Scrollable {
return element.child
}
func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Scroll) HandleChildScrollBoundsChange (ability.Scrollable) {
element.updateEnabled()
viewportBounds := element.child.ScrollViewportBounds()
contentBounds := element.child.ScrollContentBounds()
if element.horizontal != nil {
element.horizontal.SetBounds(contentBounds, viewportBounds)
}
if element.vertical != nil {
element.vertical.SetBounds(contentBounds, viewportBounds)
}
}
func (element *Scroll) HandleScroll (
position image.Point,
deltaX, deltaY float64,
modifiers input.Modifiers,
) {
horizontal, vertical := element.child.ScrollAxes()
if !horizontal { deltaX = 0 }
if !vertical { deltaY = 0 }
element.scrollChildBy(int(deltaX), int(deltaY))
}
func (element *Scroll) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Scroll) updateMinimumSize () {
var width, height int
if element.child != nil {
width, height = element.entity.ChildMinimumSize (
element.entity.IndexOf(element.child))
}
if element.horizontal != nil {
hMinWidth, hMinHeight := element.entity.ChildMinimumSize (
element.entity.IndexOf(element.horizontal))
height += hMinHeight
if hMinWidth > width {
width = hMinWidth
}
}
if element.vertical != nil {
vMinWidth, vMinHeight := element.entity.ChildMinimumSize (
element.entity.IndexOf(element.vertical))
width += vMinWidth
if vMinHeight > height {
height = vMinHeight
}
}
element.entity.SetMinimumSize(width, height)
}
func (element *Scroll) updateEnabled () {
horizontal, vertical := false, false
if element.child != nil {
horizontal, vertical = element.child.ScrollAxes()
}
if element.horizontal != nil {
element.horizontal.SetEnabled(horizontal)
}
if element.vertical != nil {
element.vertical.SetEnabled(vertical)
}
}
func (element *Scroll) scrollChildBy (x, y int) {
if element.child == nil { return }
scrollPoint :=
element.child.ScrollViewportBounds().Min.
Add(image.Pt(x, y))
element.child.ScrollTo(scrollPoint)
}

321
elements/scrollbar.go Normal file
View File

@ -0,0 +1,321 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
// ScrollBar is an element similar to Slider, but it has special behavior that
// makes it well suited for controlling the viewport position on one axis of a
// scrollable element. Instead of having a value from zero to one, it stores
// viewport and content boundaries. When the user drags the scroll bar handle,
// the scroll bar calls the OnScroll callback assigned to it with the position
// the user is trying to move the handle to. A program can check to see if this
// value is valid, move the viewport, and give the scroll bar the new viewport
// bounds (which will then cause it to move the handle).
//
// Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is
// better for most cases.
type ScrollBar struct {
entity tomo.Entity
c tomo.Case
vertical bool
enabled bool
dragging bool
dragOffset image.Point
track image.Rectangle
bar image.Rectangle
contentBounds image.Rectangle
viewportBounds image.Rectangle
onScroll func (viewport image.Point)
}
// NewVScrollBar creates a new vertical scroll bar.
func NewVScrollBar () (element *ScrollBar) {
element = &ScrollBar {
vertical: true,
enabled: true,
}
element.c = tomo.C("tomo", "scrollBarVertical")
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
// NewHScrollBar creates a new horizontal scroll bar.
func NewHScrollBar () (element *ScrollBar) {
element = &ScrollBar {
enabled: true,
}
element.c = tomo.C("tomo", "scrollBarHorizontal")
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *ScrollBar) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ScrollBar) Draw (destination art.Canvas) {
element.recalculate()
bounds := element.entity.Bounds()
state := tomo.State {
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw (
destination,
bounds)
element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw (
destination,
element.bar)
}
func (element *ScrollBar) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
velocity := element.entity.Config().ScrollVelocity()
if position.In(element.bar) {
// the mouse is pressed down within the bar's handle
element.dragging = true
element.entity.Invalidate()
element.dragOffset =
position.Sub(element.bar.Min).
Add(element.entity.Bounds().Min)
element.dragTo(position)
} else {
// the mouse is pressed down within the bar's gutter
switch button {
case input.ButtonLeft:
// start scrolling at this point, but set the offset to
// the middle of the handle
element.dragging = true
element.dragOffset = element.fallbackDragOffset()
element.dragTo(position)
case input.ButtonMiddle:
// page up/down on middle click
viewport := 0
if element.vertical {
viewport = element.viewportBounds.Dy()
} else {
viewport = element.viewportBounds.Dx()
}
if element.isAfterHandle(position) {
element.scrollBy(viewport)
} else {
element.scrollBy(-viewport)
}
case input.ButtonRight:
// inch up/down on right click
if element.isAfterHandle(position) {
element.scrollBy(velocity)
} else {
element.scrollBy(-velocity)
}
}
}
}
func (element *ScrollBar) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if element.dragging {
element.dragging = false
element.entity.Invalidate()
}
}
func (element *ScrollBar) HandleMotion (position image.Point) {
if element.dragging {
element.dragTo(position)
}
}
func (element *ScrollBar) HandleScroll (
position image.Point,
deltaX, deltaY float64,
modifiers input.Modifiers,
) {
if element.vertical {
element.scrollBy(int(deltaY))
} else {
element.scrollBy(int(deltaX))
}
}
// SetEnabled sets whether or not the scroll bar can be interacted with.
func (element *ScrollBar) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// Enabled returns whether or not the element is enabled.
func (element *ScrollBar) Enabled () (enabled bool) {
return element.enabled
}
// SetBounds sets the content and viewport bounds of the scroll bar.
func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) {
element.contentBounds = content
element.viewportBounds = viewport
element.entity.Invalidate()
}
// OnScroll sets a function to be called when the user tries to move the scroll
// bar's handle. The callback is passed a point representing the new viewport
// position. For the scroll bar's position to visually update, the callback must
// check if the position is valid and call ScrollBar.SetBounds with the new
// viewport bounds.
func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) {
element.onScroll = callback
}
func (element *ScrollBar) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *ScrollBar) isAfterHandle (point image.Point) bool {
if element.vertical {
return point.Y > element.bar.Min.Y
} else {
return point.X > element.bar.Min.X
}
}
func (element *ScrollBar) fallbackDragOffset () image.Point {
if element.vertical {
return element.entity.Bounds().Min.
Add(image.Pt(0, element.bar.Dy() / 2))
} else {
return element.entity.Bounds().Min.
Add(image.Pt(element.bar.Dx() / 2, 0))
}
}
func (element *ScrollBar) scrollBy (delta int) {
deltaPoint := image.Point { }
if element.vertical {
deltaPoint.Y = delta
} else {
deltaPoint.X = delta
}
if element.onScroll != nil {
element.onScroll(element.viewportBounds.Min.Add(deltaPoint))
}
}
func (element *ScrollBar) dragTo (point image.Point) {
point = point.Sub(element.dragOffset)
var scrollX, scrollY float64
if element.vertical {
ratio :=
float64(element.contentBounds.Dy()) /
float64(element.track.Dy())
scrollX = float64(element.viewportBounds.Min.X)
scrollY = float64(point.Y) * ratio
} else {
ratio :=
float64(element.contentBounds.Dx()) /
float64(element.track.Dx())
scrollX = float64(point.X) * ratio
scrollY = float64(element.viewportBounds.Min.Y)
}
if element.onScroll != nil {
element.onScroll(image.Pt(int(scrollX), int(scrollY)))
}
}
func (element *ScrollBar) recalculate () {
if element.vertical {
element.recalculateVertical()
} else {
element.recalculateHorizontal()
}
}
func (element *ScrollBar) recalculateVertical () {
bounds := element.entity.Bounds()
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
element.track = padding.Apply(bounds)
contentBounds := element.contentBounds
viewportBounds := element.viewportBounds
if element.Enabled() {
element.bar.Min.X = element.track.Min.X
element.bar.Max.X = element.track.Max.X
ratio :=
float64(element.track.Dy()) /
float64(contentBounds.Dy())
element.bar.Min.Y = int(float64(viewportBounds.Min.Y) * ratio)
element.bar.Max.Y = int(float64(viewportBounds.Max.Y) * ratio)
element.bar.Min.Y += element.track.Min.Y
element.bar.Max.Y += element.track.Min.Y
}
// if the handle is out of bounds, don't display it
if element.bar.Dy() >= element.track.Dy() {
element.bar = image.Rectangle { }
}
}
func (element *ScrollBar) recalculateHorizontal () {
bounds := element.entity.Bounds()
padding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
element.track = padding.Apply(bounds)
contentBounds := element.contentBounds
viewportBounds := element.viewportBounds
if element.Enabled() {
element.bar.Min.Y = element.track.Min.Y
element.bar.Max.Y = element.track.Max.Y
ratio :=
float64(element.track.Dx()) /
float64(contentBounds.Dx())
element.bar.Min.X = int(float64(viewportBounds.Min.X) * ratio)
element.bar.Max.X = int(float64(viewportBounds.Max.X) * ratio)
element.bar.Min.X += element.track.Min.X
element.bar.Max.X += element.track.Min.X
}
// if the handle is out of bounds, don't display it
if element.bar.Dx() >= element.track.Dx() {
element.bar = image.Rectangle { }
}
}
func (element *ScrollBar) updateMinimumSize () {
gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c)
if element.vertical {
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
} else {
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
gutterPadding.Vertical() + handlePadding.Vertical())
}
}

260
elements/slider.go Normal file
View File

@ -0,0 +1,260 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
// Slider is a slider control with a floating point value between zero and one.
type Slider struct {
slider
}
// NewVSlider creates a new horizontal slider with the specified value.
func NewVSlider (value float64) (element *Slider) {
element = NewHSlider(value)
element.vertical = true
return
}
// NewHSlider creates a new horizontal slider with the specified value.
func NewHSlider (value float64) (element *Slider) {
element = &Slider { }
element.value = value
element.entity = tomo.GetBackend().NewEntity(element)
element.construct()
return
}
type slider struct {
entity tomo.Entity
c tomo.Case
value float64
vertical bool
dragging bool
enabled bool
dragOffset int
track image.Rectangle
bar image.Rectangle
onSlide func ()
onRelease func ()
}
func (element *slider) construct () {
element.enabled = true
if element.vertical {
element.c = tomo.C("tomo", "sliderVertical")
} else {
element.c = tomo.C("tomo", "sliderHorizontal")
}
element.updateMinimumSize()
}
// Entity returns this element's entity.
func (element *slider) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *slider) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
element.track = element.entity.Theme().Padding(tomo.PatternGutter, element.c).Apply(bounds)
if element.vertical {
barSize := element.track.Dx()
element.bar = image.Rect(0, 0, barSize, barSize).Add(element.track.Min)
barOffset :=
float64(element.track.Dy() - barSize) *
(1 - element.value)
element.bar = element.bar.Add(image.Pt(0, int(barOffset)))
} else {
barSize := element.track.Dy()
element.bar = image.Rect(0, 0, barSize, barSize).Add(element.track.Min)
barOffset :=
float64(element.track.Dx() - barSize) *
element.value
element.bar = element.bar.Add(image.Pt(int(barOffset), 0))
}
state := tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.dragging,
}
element.entity.Theme().Pattern(tomo.PatternGutter, state, element.c).Draw(destination, bounds)
element.entity.Theme().Pattern(tomo.PatternHandle, state, element.c).Draw(destination, element.bar)
}
// Focus gives this element input focus.
func (element *slider) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this slider can be dragged or not.
func (element *slider) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this slider can be dragged or not.
func (element *slider) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
func (element *slider) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *slider) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
element.Focus()
if button == input.ButtonLeft {
element.dragging = true
element.value = element.valueFor(position.X, position.Y)
if element.onSlide != nil {
element.onSlide()
}
element.entity.Invalidate()
}
}
func (element *slider) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button != input.ButtonLeft || !element.dragging { return }
element.dragging = false
if element.onRelease != nil {
element.onRelease()
}
element.entity.Invalidate()
}
func (element *slider) HandleMotion (position image.Point) {
if element.dragging {
element.dragging = true
element.value = element.valueFor(position.X, position.Y)
if element.onSlide != nil {
element.onSlide()
}
element.entity.Invalidate()
}
}
func (element *slider) HandleScroll (
position image.Point,
deltaX, deltaY float64,
modifiers input.Modifiers,
) { }
func (element *slider) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyUp:
element.changeValue(0.1)
case input.KeyDown:
element.changeValue(-0.1)
case input.KeyRight:
if element.vertical {
element.changeValue(-0.1)
} else {
element.changeValue(0.1)
}
case input.KeyLeft:
if element.vertical {
element.changeValue(0.1)
} else {
element.changeValue(-0.1)
}
}
}
func (element *slider) HandleKeyUp (key input.Key, modifiers input.Modifiers) { }
// Value returns the slider's value.
func (element *slider) Value () (value float64) {
return element.value
}
// SetValue sets the slider's value.
func (element *slider) SetValue (value float64) {
if value < 0 { value = 0 }
if value > 1 { value = 1 }
if element.value == value { return }
element.value = value
if element.onRelease != nil {
element.onRelease()
}
element.entity.Invalidate()
}
// OnSlide sets a function to be called every time the slider handle changes
// position while being dragged.
func (element *slider) OnSlide (callback func ()) {
element.onSlide = callback
}
// OnRelease sets a function to be called when the handle stops being dragged.
func (element *slider) OnRelease (callback func ()) {
element.onRelease = callback
}
func (element *slider) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *slider) changeValue (delta float64) {
element.value += delta
if element.value < 0 {
element.value = 0
}
if element.value > 1 {
element.value = 1
}
if element.onRelease != nil {
element.onRelease()
}
element.entity.Invalidate()
}
func (element *slider) valueFor (x, y int) (value float64) {
if element.vertical {
value =
float64(y - element.track.Min.Y - element.bar.Dy() / 2) /
float64(element.track.Dy() - element.bar.Dy())
value = 1 - value
} else {
value =
float64(x - element.track.Min.X - element.bar.Dx() / 2) /
float64(element.track.Dx() - element.bar.Dx())
}
if value < 0 { value = 0 }
if value > 1 { value = 1 }
return
}
func (element *slider) updateMinimumSize () {
gutterPadding := element.entity.Theme().Padding(tomo.PatternGutter, element.c)
handlePadding := element.entity.Theme().Padding(tomo.PatternHandle, element.c)
if element.vertical {
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
} else {
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
gutterPadding.Vertical() + handlePadding.Vertical())
}
}

72
elements/spacer.go Normal file
View File

@ -0,0 +1,72 @@
package elements
import "tomo"
import "art"
var spacerCase = tomo.C("tomo", "spacer")
// Spacer can be used to put space between two elements..
type Spacer struct {
entity tomo.Entity
line bool
}
// NewSpacer creates a new spacer.
func NewSpacer () (element *Spacer) {
element = &Spacer { }
element.entity = tomo.GetBackend().NewEntity(element)
element.updateMinimumSize()
return
}
// NewLine creates a new line separator.
func NewLine () (element *Spacer) {
element = NewSpacer()
element.SetLine(true)
return
}
// Entity returns this element's entity.
func (element *Spacer) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Spacer) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
if element.line {
pattern := element.entity.Theme().Pattern (
tomo.PatternLine,
tomo.State { }, spacerCase)
pattern.Draw(destination, bounds)
} else {
pattern := element.entity.Theme().Pattern (
tomo.PatternBackground,
tomo.State { }, spacerCase)
pattern.Draw(destination, bounds)
}
}
/// SetLine sets whether or not the spacer will appear as a colored line.
func (element *Spacer) SetLine (line bool) {
if element.line == line { return }
element.line = line
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Spacer) HandleThemeChange () {
element.entity.Invalidate()
}
func (element *Spacer) updateMinimumSize () {
if element.line {
padding := element.entity.Theme().Padding(tomo.PatternLine, spacerCase)
element.entity.SetMinimumSize (
padding.Horizontal(),
padding.Vertical())
} else {
element.entity.SetMinimumSize(1, 1)
}
}

206
elements/switch.go Normal file
View File

@ -0,0 +1,206 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
import "tomo/textdraw"
var switchCase = tomo.C("tomo", "switch")
// Switch is a toggle-able on/off switch with an optional label. It is
// functionally identical to Checkbox, but plays a different semantic role.
type Switch struct {
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
pressed bool
checked bool
text string
onToggle func ()
}
// NewSwitch creates a new switch with the specified label text.
func NewSwitch (text string, on bool) (element *Switch) {
element = &Switch {
checked: on,
text: text,
enabled: true,
}
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal, switchCase))
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *Switch) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Switch) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
state := tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
On: element.checked,
}
element.entity.DrawBackground(destination)
if element.checked {
handleBounds.Min.X += bounds.Dy()
handleBounds.Max.X += bounds.Dy()
if element.pressed {
handleBounds.Min.X -= 2
handleBounds.Max.X -= 2
}
} else {
if element.pressed {
handleBounds.Min.X += 2
handleBounds.Max.X += 2
}
}
gutterPattern := element.entity.Theme().Pattern (
tomo.PatternGutter, state, switchCase)
gutterPattern.Draw(destination, gutterBounds)
handlePattern := element.entity.Theme().Pattern (
tomo.PatternHandle, state, switchCase)
handlePattern.Draw(destination, handleBounds)
textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() * 2 +
element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X,
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, switchCase)
element.drawer.Draw(destination, foreground, offset)
}
func (element *Switch) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *Switch) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
element.entity.Invalidate()
}
func (element *Switch) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false
within := position.In(element.entity.Bounds())
if within {
element.checked = !element.checked
}
element.entity.Invalidate()
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
element.entity.Invalidate()
if element.onToggle != nil {
element.onToggle()
}
}
}
// OnToggle sets the function to be called when the switch is flipped.
func (element *Switch) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the switch is currently on.
func (element *Switch) Value () (on bool) {
return element.checked
}
// Focus gives this element input focus.
func (element *Switch) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this switch is enabled or not.
func (element *Switch) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this switch can be toggled or not.
func (element *Switch) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// SetText sets the checkbox's label text.
func (element *Switch) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Switch) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal, switchCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Switch) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
lineHeight := element.drawer.LineHeight().Round()
if element.text == "" {
element.entity.SetMinimumSize(lineHeight * 2, lineHeight)
} else {
element.entity.SetMinimumSize (
lineHeight * 2 +
element.entity.Theme().Margin(tomo.PatternBackground, switchCase).X +
textBounds.Dx(),
lineHeight)
}
}

View File

@ -4,71 +4,73 @@ import "fmt"
import "time"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
import "tomo"
import "art"
import "art/shatter"
import "tomo/textdraw"
import "art/shapes"
import "art/artutil"
import "art/patterns"
// Artist is an element that displays shapes and patterns drawn by the artist
// Artist is an element that displays shapes and patterns drawn by the art
// package in order to test it.
type Artist struct {
*core.Core
core core.CoreControl
entity tomo.Entity
}
// NewArtist creates a new artist test element.
// NewArtist creates a new art test element.
func NewArtist () (element *Artist) {
element = &Artist { }
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(240, 240)
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(240, 240)
return
}
func (element *Artist) draw () {
bounds := element.Bounds()
patterns.Uhex(0x000000FF).Draw(element.core, bounds)
func (element *Artist) Entity () tomo.Entity {
return element.entity
}
func (element *Artist) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
patterns.Uhex(0x000000FF).Draw(destination, bounds)
drawStart := time.Now()
// 0, 0 - 3, 0
for x := 0; x < 4; x ++ {
element.colorLines(x + 1, element.cellAt(x, 0).Bounds())
element.colorLines(destination, x + 1, element.cellAt(destination, x, 0).Bounds())
}
// 4, 0
c40 := element.cellAt(4, 0)
shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
c40 := element.cellAt(destination, 4, 0)
shapes.StrokeColorRectangle(c40, artutil.Hex(0x888888FF), c40.Bounds(), 1)
shapes.ColorLine (
c40, artist.Hex(0xFF0000FF), 1,
c40, artutil.Hex(0xFF0000FF), 1,
c40.Bounds().Min, c40.Bounds().Max)
// 0, 1
c01 := element.cellAt(0, 1)
shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1)
shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds())
c01 := element.cellAt(destination, 0, 1)
shapes.StrokeColorRectangle(c01, artutil.Hex(0x888888FF), c01.Bounds(), 1)
shapes.FillColorEllipse(destination, artutil.Hex(0x00FF00FF), c01.Bounds())
// 1, 1 - 3, 1
for x := 1; x < 4; x ++ {
c := element.cellAt(x, 1)
c := element.cellAt(destination, x, 1)
shapes.StrokeColorRectangle (
element.core, artist.Hex(0x888888FF),
destination, artutil.Hex(0x888888FF),
c.Bounds(), 1)
shapes.StrokeColorEllipse (
element.core,
destination,
[]color.RGBA {
artist.Hex(0xFF0000FF),
artist.Hex(0x00FF00FF),
artist.Hex(0xFF00FFFF),
artutil.Hex(0xFF0000FF),
artutil.Hex(0x00FF00FF),
artutil.Hex(0xFF00FFFF),
} [x - 1],
c.Bounds(), x)
}
// 4, 1
c41 := element.cellAt(4, 1)
c41 := element.cellAt(destination, 4, 1)
shatterPos := c41.Bounds().Min
rocks := []image.Rectangle {
image.Rect(3, 12, 13, 23).Add(shatterPos),
@ -79,117 +81,129 @@ func (element *Artist) draw () {
}
tiles := shatter.Shatter(c41.Bounds(), rocks...)
for index, tile := range tiles {
artist.DrawBounds (
element.core,
[]artist.Pattern {
patterns.Uhex(0xFF0000FF),
patterns.Uhex(0x00FF00FF),
patterns.Uhex(0xFF00FFFF),
patterns.Uhex(0xFFFF00FF),
patterns.Uhex(0x00FFFFFF),
} [index % 5], tile)
[]art.Pattern {
patterns.Uhex(0xFF0000FF),
patterns.Uhex(0x00FF00FF),
patterns.Uhex(0xFF00FFFF),
patterns.Uhex(0xFFFF00FF),
patterns.Uhex(0x00FFFFFF),
} [index % 5].Draw(destination, tile)
}
// 0, 2
c02 := element.cellAt(0, 2)
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
shapes.FillEllipse(c02, c41)
c02 := element.cellAt(destination, 0, 2)
shapes.StrokeColorRectangle(c02, artutil.Hex(0x888888FF), c02.Bounds(), 1)
shapes.FillEllipse(c02, c41, c02.Bounds())
// 1, 2
c12 := element.cellAt(1, 2)
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
shapes.StrokeEllipse(c12, c41, 5)
c12 := element.cellAt(destination, 1, 2)
shapes.StrokeColorRectangle(c12, artutil.Hex(0x888888FF), c12.Bounds(), 1)
shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
// 2, 2
c22 := element.cellAt(2, 2)
shapes.FillRectangle(c22, c41)
c22 := element.cellAt(destination, 2, 2)
shapes.FillRectangle(c22, c41, c22.Bounds())
// 3, 2
c32 := element.cellAt(3, 2)
shapes.StrokeRectangle(c32, c41, 5)
c32 := element.cellAt(destination, 3, 2)
shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
// 4, 2
c42 := element.cellAt(4, 2)
c42 := element.cellAt(destination, 4, 2)
// 0, 3
c03 := element.cellAt(0, 3)
c03 := element.cellAt(destination, 0, 3)
patterns.Border {
Canvas: element.thingy(c42),
Inset: artist.Inset { 8, 8, 8, 8 },
Inset: art.Inset { 8, 8, 8, 8 },
}.Draw(c03, c03.Bounds())
// 1, 3
c13 := element.cellAt(1, 3)
c13 := element.cellAt(destination, 1, 3)
patterns.Border {
Canvas: element.thingy(c42),
Inset: artist.Inset { 8, 8, 8, 8 },
Inset: art.Inset { 8, 8, 8, 8 },
}.Draw(c13, c13.Bounds().Inset(10))
// 2, 3
c23 := element.cellAt(destination, 2, 3)
patterns.Border {
Canvas: element.thingy(c42),
Inset: art.Inset { 8, 8, 8, 8 },
}.Draw(c23, c23.Bounds())
patterns.Border {
Canvas: element.thingy(c42),
Inset: art.Inset { 8, 8, 8, 8 },
}.Draw(art.Cut(c23, c23.Bounds().Inset(16)), c23.Bounds())
// how long did that take to render?
drawTime := time.Since(drawStart)
textDrawer := textdraw.Drawer { }
textDrawer.SetFace(defaultfont.FaceRegular)
textDrawer.SetFace(element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
tomo.C("tomo", "art")))
textDrawer.SetText ([]rune (fmt.Sprintf (
"%dms\n%dus",
drawTime.Milliseconds(),
drawTime.Microseconds())))
textDrawer.Draw (
element.core, artist.Hex(0xFFFFFFFF),
destination, artutil.Hex(0xFFFFFFFF),
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
}
func (element *Artist) colorLines (weight int, bounds image.Rectangle) {
func (element *Artist) colorLines (destination art.Canvas, weight int, bounds image.Rectangle) {
bounds = bounds.Inset(4)
c := artist.Hex(0xFFFFFFFF)
shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max)
c := artutil.Hex(0xFFFFFFFF)
shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max)
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y),
image.Pt(bounds.Min.X, bounds.Max.Y))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
}
func (element *Artist) cellAt (x, y int) (canvas.Canvas) {
bounds := element.Bounds()
func (element *Artist) cellAt (destination art.Canvas, x, y int) (art.Canvas) {
bounds := element.entity.Bounds()
cellBounds := image.Rectangle { }
cellBounds.Min = bounds.Min
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4
return canvas.Cut (element.core, cellBounds.Add (image.Pt (
return art.Cut (destination, cellBounds.Add (image.Pt (
x * cellBounds.Dx(),
y * cellBounds.Dy())))
}
func (element *Artist) thingy (destination canvas.Canvas) (result canvas.Canvas) {
func (element *Artist) thingy (destination art.Canvas) (result art.Canvas) {
bounds := destination.Bounds()
bounds = image.Rect(0, 0, 32, 32).Add(bounds.Min)
shapes.FillColorRectangle(destination, artist.Hex(0x440000FF), bounds)
shapes.StrokeColorRectangle(destination, artist.Hex(0xFF0000FF), bounds, 1)
shapes.StrokeColorRectangle(destination, artist.Hex(0x004400FF), bounds.Inset(4), 1)
shapes.FillColorRectangle(destination, artist.Hex(0x004444FF), bounds.Inset(12))
shapes.StrokeColorRectangle(destination, artist.Hex(0x888888FF), bounds.Inset(8), 1)
return canvas.Cut(destination, bounds)
shapes.FillColorRectangle(destination, artutil.Hex(0x440000FF), bounds)
shapes.StrokeColorRectangle(destination, artutil.Hex(0xFF0000FF), bounds, 1)
shapes.StrokeColorRectangle(destination, artutil.Hex(0x004400FF), bounds.Inset(4), 1)
shapes.FillColorRectangle(destination, artutil.Hex(0x004444FF), bounds.Inset(12))
shapes.StrokeColorRectangle(destination, artutil.Hex(0x888888FF), bounds.Inset(8), 1)
return art.Cut(destination, bounds)
}

View File

@ -1,94 +1,86 @@
package testing
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "tomo"
import "tomo/input"
import "art"
import "art/shapes"
import "art/artutil"
var mouseCase = tomo.C("tomo", "mouse")
// Mouse is an element capable of testing mouse input. When the mouse is clicked
// and dragged on it, it draws a trail.
type Mouse struct {
*core.Core
core core.CoreControl
drawing bool
entity tomo.Entity
pressed bool
lastMousePos image.Point
config config.Config
theme theme.Theme
c theme.Case
}
// NewMouse creates a new mouse test element.
func NewMouse () (element *Mouse) {
element = &Mouse { c: theme.C("testing", "mouse") }
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(32, 32)
element = &Mouse { }
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(32, 32)
return
}
// SetTheme sets the element's theme.
func (element *Mouse) SetTheme (new theme.Theme) {
element.theme = new
element.redo()
func (element *Mouse) Entity () tomo.Entity {
return element.entity
}
// SetConfig sets the element's configuration.
func (element *Mouse) SetConfig (new config.Config) {
element.config = new
element.redo()
}
func (element *Mouse) redo () {
if !element.core.HasImage() { return }
element.draw()
element.core.DamageAll()
}
func (element *Mouse) draw () {
bounds := element.Bounds()
accent := element.theme.Color (
theme.ColorAccent,
theme.State { },
element.c)
shapes.FillColorRectangle(element.core, accent, bounds)
func (element *Mouse) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
accent := element.entity.Theme().Color (
tomo.ColorAccent,
tomo.State { },
mouseCase)
shapes.FillColorRectangle(destination, accent, bounds)
shapes.StrokeColorRectangle (
element.core,
artist.Hex(0x000000FF),
destination,
artutil.Hex(0x000000FF),
bounds, 1)
shapes.ColorLine (
element.core, artist.Hex(0xFFFFFFFF), 1,
destination, artutil.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
shapes.ColorLine (
element.core, artist.Hex(0xFFFFFFFF), 1,
destination, artutil.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
if element.pressed {
midpoint := bounds.Min.Add(bounds.Max.Sub(bounds.Min).Div(2))
shapes.ColorLine (
destination, artutil.Hex(0x000000FF), 1,
midpoint, element.lastMousePos)
}
}
func (element *Mouse) HandleMouseDown (x, y int, button input.Button) {
element.drawing = true
element.lastMousePos = image.Pt(x, y)
func (element *Mouse) HandleThemeChange (new tomo.Theme) {
element.entity.Invalidate()
}
func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
element.drawing = false
mousePos := image.Pt(x, y)
element.core.DamageRegion (shapes.ColorLine (
element.core, artist.Hex(0x000000FF), 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
func (element *Mouse) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
element.pressed = true
element.lastMousePos = position
element.entity.Invalidate()
}
func (element *Mouse) HandleMouseMove (x, y int) {
if !element.drawing { return }
mousePos := image.Pt(x, y)
element.core.DamageRegion (shapes.ColorLine (
element.core, artist.Hex(0x000000FF), 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
func (element *Mouse) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
element.pressed = false
element.entity.Invalidate()
}
func (element *Mouse) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Mouse) HandleMotion (position image.Point) {
if !element.pressed { return }
element.lastMousePos = position
element.entity.Invalidate()
}

576
elements/textbox.go Normal file
View File

@ -0,0 +1,576 @@
package elements
import "io"
import "time"
import "image"
import "tomo"
import "tomo/data"
import "tomo/input"
import "art"
import "tomo/textdraw"
import "tomo/textmanip"
import "tomo/fixedutil"
import "art/shapes"
var textBoxCase = tomo.C("tomo", "textBox")
// TextBox is a single-line text input.
type TextBox struct {
entity tomo.Entity
enabled bool
lastClick time.Time
dragging int
dot textmanip.Dot
scroll int
placeholder string
text []rune
placeholderDrawer textdraw.Drawer
valueDrawer textdraw.Drawer
onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool)
onChange func ()
onEnter func ()
onScrollBoundsChange func ()
}
// NewTextBox creates a new text box with the specified placeholder text, and
// a value. When the value is empty, the placeholder will be displayed in gray
// text.
func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { enabled: true }
element.entity = tomo.GetBackend().NewEntity(element)
element.placeholder = placeholder
element.placeholderDrawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal, textBoxCase))
element.valueDrawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal, textBoxCase))
element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize()
element.SetValue(value)
return
}
// Entity returns this element's entity.
func (element *TextBox) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *TextBox) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
state := element.state()
pattern := element.entity.Theme().Pattern(tomo.PatternInput, state, textBoxCase)
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
innerCanvas := art.Cut(destination, padding.Apply(bounds))
pattern.Draw(destination, bounds)
offset := element.textOffset()
if element.entity.Focused() && !element.dot.Empty() {
// draw selection bounds
accent := element.entity.Theme().Color(tomo.ColorAccent, state, textBoxCase)
canon := element.dot.Canon()
foff := fixedutil.Pt(offset)
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
end := element.valueDrawer.PositionAt(canon.End).Add(foff)
end.Y += element.valueDrawer.LineHeight()
shapes.FillColorRectangle (
innerCanvas,
accent,
image.Rectangle {
fixedutil.RoundPt(start),
fixedutil.RoundPt(end),
})
}
if len(element.text) == 0 {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
foreground := element.entity.Theme().Color (
tomo.ColorForeground,
tomo.State { Disabled: true }, textBoxCase)
element.placeholderDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
} else {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
element.valueDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
}
if element.entity.Focused() && element.dot.Empty() {
// draw cursor
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, textBoxCase)
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
shapes.ColorLine (
innerCanvas,
foreground, 1,
cursorPosition.Add(offset),
image.Pt (
cursorPosition.X,
cursorPosition.Y + element.valueDrawer.
LineHeight().Round()).Add(offset))
}
}
// Layout causes the element to perform a layout operation.
func (element *TextBox) Layout () {
element.scrollToCursor()
}
func (element *TextBox) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *TextBox) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
element.Focus()
switch button {
case input.ButtonLeft:
runeIndex := element.atPosition(position)
if runeIndex == -1 { return }
if time.Since(element.lastClick) < element.entity.Config().DoubleClickDelay() {
element.dragging = 2
element.dot = textmanip.WordAround(element.text, runeIndex)
} else {
element.dragging = 1
element.dot = textmanip.EmptyDot(runeIndex)
element.lastClick = time.Now()
}
element.entity.Invalidate()
case input.ButtonRight:
element.contextMenu(position)
}
}
func (element *TextBox) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button == input.ButtonLeft {
element.dragging = 0
}
}
func (element *TextBox) HandleMotion (position image.Point) {
if !element.Enabled() { return }
switch element.dragging {
case 1:
runeIndex := element.atPosition(position)
if runeIndex > -1 {
element.dot.End = runeIndex
element.entity.Invalidate()
}
case 2:
runeIndex := element.atPosition(position)
if runeIndex > -1 {
if runeIndex < element.dot.Start {
element.dot.End =
runeIndex -
textmanip.WordToLeft (
element.text,
runeIndex)
} else {
element.dot.End =
runeIndex +
textmanip.WordToRight (
element.text,
runeIndex)
}
element.entity.Invalidate()
}
}
}
func (element *TextBox) textOffset () image.Point {
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
bounds := element.entity.Bounds()
innerBounds := padding.Apply(bounds)
textHeight := element.valueDrawer.LineHeight().Round()
return bounds.Min.Add (image.Pt (
padding[art.SideLeft] - element.scroll,
padding[art.SideTop] + (innerBounds.Dy() - textHeight) / 2))
}
func (element *TextBox) atPosition (position image.Point) int {
offset := element.textOffset()
textBoundsMin := element.valueDrawer.LayoutBounds().Min
return element.valueDrawer.AtPosition (
fixedutil.Pt(position.Sub(offset).Add(textBoundsMin)))
}
func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) {
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
return
}
scrollMemory := element.scroll
textChanged := false
switch {
case key == input.KeyEnter:
if element.onEnter != nil {
element.onEnter()
}
case key == input.KeyBackspace:
if len(element.text) < 1 { break }
element.text, element.dot = textmanip.Backspace (
element.text,
element.dot,
modifiers.Control)
textChanged = true
case key == input.KeyDelete:
if len(element.text) < 1 { break }
element.text, element.dot = textmanip.Delete (
element.text,
element.dot,
modifiers.Control)
textChanged = true
case key == input.KeyLeft:
if modifiers.Shift {
element.dot = textmanip.SelectLeft (
element.text,
element.dot,
modifiers.Control)
} else {
element.dot = textmanip.MoveLeft (
element.text,
element.dot,
modifiers.Control)
}
element.scrollToCursor()
element.entity.Invalidate()
case key == input.KeyRight:
if modifiers.Shift {
element.dot = textmanip.SelectRight (
element.text,
element.dot,
modifiers.Control)
} else {
element.dot = textmanip.MoveRight (
element.text,
element.dot,
modifiers.Control)
}
element.scrollToCursor()
element.entity.Invalidate()
case key == 'a' && modifiers.Control:
element.dot.Start = 0
element.dot.End = len(element.text)
element.scrollToCursor()
element.entity.Invalidate()
case key == 'x' && modifiers.Control: element.Cut()
case key == 'c' && modifiers.Control: element.Copy()
case key == 'v' && modifiers.Control: element.Paste()
case key == input.KeyMenu:
pos := fixedutil.RoundPt(element.valueDrawer.PositionAt(element.dot.End)).
Add(element.textOffset())
pos.Y += element.valueDrawer.LineHeight().Round()
element.contextMenu(pos)
case key.Printable():
element.text, element.dot = textmanip.Type (
element.text,
element.dot,
rune(key))
textChanged = true
}
if textChanged {
element.runOnChange()
element.valueDrawer.SetText(element.text)
element.scrollToCursor()
element.entity.Invalidate()
}
if (textChanged || scrollMemory != element.scroll) {
element.entity.NotifyScrollBoundsChange()
}
}
// Cut cuts the selected text in the text box and places it in the clipboard.
func (element *TextBox) Cut () {
var lifted []rune
element.text, element.dot, lifted = textmanip.Lift (
element.text,
element.dot)
if lifted != nil {
element.clipboardPut(lifted)
element.notifyAsyncTextChange()
}
}
// Copy copies the selected text in the text box and places it in the clipboard.
func (element *TextBox) Copy () {
element.clipboardPut(element.dot.Slice(element.text))
}
// Paste pastes text data from the clipboard into the text box.
func (element *TextBox) Paste () {
window := element.entity.Window()
if window == nil { return }
window.Paste (func (d data.Data, err error) {
if err != nil { return }
reader, ok := d[data.MimePlain]
if !ok { return }
bytes, _ := io.ReadAll(reader)
element.text, element.dot = textmanip.Type (
element.text,
element.dot,
[]rune(string(bytes))...)
element.notifyAsyncTextChange()
})
}
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
// SetPlaceholder sets the element's placeholder text.
func (element *TextBox) SetPlaceholder (placeholder string) {
if element.placeholder == placeholder { return }
element.placeholder = placeholder
element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetValue sets the input's value.
func (element *TextBox) SetValue (text string) {
// if element.text == text { return }
element.text = []rune(text)
element.runOnChange()
element.valueDrawer.SetText(element.text)
if element.dot.End > element.valueDrawer.Length() {
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
}
element.scrollToCursor()
element.entity.Invalidate()
}
// Value returns the input's value.
func (element *TextBox) Value () (value string) {
return string(element.text)
}
// Filled returns whether or not this element has a value.
func (element *TextBox) Filled () (filled bool) {
return len(element.text) > 0
}
// OnKeyDown specifies a function to be called when a key is pressed within the
// text input.
func (element *TextBox) OnKeyDown (
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
) {
element.onKeyDown = callback
}
// OnEnter specifies a function to be called when the enter key is pressed
// within this input.
func (element *TextBox) OnEnter (callback func ()) {
element.onEnter = callback
}
// OnChange specifies a function to be called when the value of this input
// changes.
func (element *TextBox) OnChange (callback func ()) {
element.onChange = callback
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *TextBox) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// Focus gives this element input focus.
func (element *TextBox) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this label can be edited or not.
func (element *TextBox) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this label can be edited or not.
func (element *TextBox) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// ScrollContentBounds returns the full content size of the element.
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
bounds = element.valueDrawer.LayoutBounds()
return bounds.Sub(bounds.Min)
}
// ScrollViewportBounds returns the size and position of the element's viewport
// relative to ScrollBounds.
func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
return image.Rect (
element.scroll,
0,
element.scroll + element.scrollViewportWidth(),
0)
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *TextBox) ScrollTo (position image.Point) {
// constrain to minimum
element.scroll = position.X
if element.scroll < 0 { element.scroll = 0 }
// constrain to maximum
contentBounds := element.ScrollContentBounds()
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
if element.scroll > maxPosition { element.scroll = maxPosition }
element.entity.Invalidate()
element.entity.NotifyScrollBoundsChange()
}
// ScrollAxes returns the supported axes for scrolling.
func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
return true, false
}
func (element *TextBox) HandleThemeChange () {
face := element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
textBoxCase)
element.placeholderDrawer.SetFace(face)
element.valueDrawer.SetFace(face)
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *TextBox) contextMenu (position image.Point) {
window := element.entity.Window()
menu, err := window.NewMenu(image.Rectangle { position, position })
if err != nil { return }
closeAnd := func (callback func ()) func () {
return func () { callback(); menu.Close() }
}
cutButton := NewButton("Cut")
cutButton.ShowText(false)
cutButton.SetIcon(tomo.IconCut)
cutButton.SetEnabled(!element.dot.Empty())
cutButton.OnClick(closeAnd(element.Cut))
copyButton := NewButton("Copy")
copyButton.ShowText(false)
copyButton.SetIcon(tomo.IconCopy)
copyButton.SetEnabled(!element.dot.Empty())
copyButton.OnClick(closeAnd(element.Copy))
pasteButton := NewButton("Paste")
pasteButton.ShowText(false)
pasteButton.SetIcon(tomo.IconPaste)
pasteButton.OnClick(closeAnd(element.Paste))
menu.Adopt (NewHBox (
SpaceNone,
pasteButton,
copyButton,
cutButton,
))
pasteButton.Focus()
menu.Show()
}
func (element *TextBox) runOnChange () {
if element.onChange != nil {
element.onChange()
}
}
func (element *TextBox) scrollViewportWidth () (width int) {
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
return padding.Apply(element.entity.Bounds()).Dx()
}
func (element *TextBox) scrollToCursor () {
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round()
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
cursorPosition.X -= element.scroll
maxX := bounds.Max.X
minX := maxX
if cursorPosition.X > maxX {
element.scroll += cursorPosition.X - maxX
element.entity.NotifyScrollBoundsChange()
element.entity.Invalidate()
} else if cursorPosition.X < minX {
element.scroll -= minX - cursorPosition.X
if element.scroll < 0 { element.scroll = 0 }
element.entity.NotifyScrollBoundsChange()
element.entity.Invalidate()
}
}
func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds()
padding := element.entity.Theme().Padding(tomo.PatternInput, textBoxCase)
element.entity.SetMinimumSize (
padding.Horizontal() + textBounds.Dx(),
padding.Vertical() +
element.placeholderDrawer.LineHeight().Round())
}
func (element *TextBox) notifyAsyncTextChange () {
element.runOnChange()
element.valueDrawer.SetText(element.text)
element.scrollToCursor()
element.entity.Invalidate()
}
func (element *TextBox) clipboardPut (text []rune) {
window := element.entity.Window()
if window != nil {
window.Copy(data.Bytes(data.MimePlain, []byte(string(text))))
}
}
func (element *TextBox) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
}
}

265
elements/togglebutton.go Normal file
View File

@ -0,0 +1,265 @@
package elements
import "image"
import "tomo"
import "tomo/input"
import "art"
import "tomo/textdraw"
var toggleButtonCase = tomo.C("tomo", "toggleButton")
// ToggleButton is a togglable button.
type ToggleButton struct {
entity tomo.Entity
drawer textdraw.Drawer
enabled bool
pressed bool
on bool
text string
showText bool
hasIcon bool
iconId tomo.Icon
onToggle func ()
}
// NewToggleButton creates a new toggle button with the specified label text.
func NewToggleButton (text string, on bool) (element *ToggleButton) {
element = &ToggleButton {
showText: true,
enabled: true,
on: on,
}
element.entity = tomo.GetBackend().NewEntity(element)
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal,
toggleButtonCase))
element.SetText(text)
return
}
// Entity returns this element's entity.
func (element *ToggleButton) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ToggleButton) Draw (destination art.Canvas) {
state := element.state()
bounds := element.entity.Bounds()
pattern := element.entity.Theme().Pattern(tomo.PatternButton, state, toggleButtonCase)
lampPattern := element.entity.Theme().Pattern(tomo.PatternLamp, state, toggleButtonCase)
lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase).Horizontal()
lampBounds := bounds
lampBounds.Max.X = lampBounds.Min.X + lampPadding
bounds.Min.X += lampPadding
pattern.Draw(destination, bounds)
lampPattern.Draw(destination, lampBounds)
foreground := element.entity.Theme().Color(tomo.ColorForeground, state, toggleButtonCase)
sink := element.entity.Theme().Sink(tomo.PatternButton, toggleButtonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase)
offset := image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2).Add(bounds.Min)
if element.showText {
textBounds := element.drawer.LayoutBounds()
offset.X -= textBounds.Dx() / 2
offset.Y -= textBounds.Dy() / 2
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
}
if element.hasIcon {
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
if icon != nil {
iconBounds := icon.Bounds()
addedWidth := iconBounds.Dx()
iconOffset := offset
if element.showText {
addedWidth += margin.X
}
iconOffset.X -= addedWidth / 2
iconOffset.Y =
bounds.Min.Y +
(bounds.Dy() -
iconBounds.Dy()) / 2
if element.pressed {
iconOffset = iconOffset.Add(sink)
}
offset.X += addedWidth / 2
icon.Draw(destination, foreground, iconOffset)
}
}
if element.showText {
if element.pressed {
offset = offset.Add(sink)
}
element.drawer.Draw(destination, foreground, offset)
}
}
// OnToggle sets the function to be called when the button is toggled.
func (element *ToggleButton) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the button is currently on.
func (element *ToggleButton) Value () (on bool) {
return element.on
}
// Focus gives this element input focus.
func (element *ToggleButton) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this button is enabled or not.
func (element *ToggleButton) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this button can be toggled or not.
func (element *ToggleButton) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// SetText sets the button's label text.
func (element *ToggleButton) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
// current icon if it exists.
func (element *ToggleButton) SetIcon (id tomo.Icon) {
if id == tomo.IconNone {
element.hasIcon = false
} else {
if element.hasIcon && element.iconId == id { return }
element.hasIcon = true
element.iconId = id
}
element.updateMinimumSize()
element.entity.Invalidate()
}
// ShowText sets whether or not the button's text will be displayed.
func (element *ToggleButton) ShowText (showText bool) {
if element.showText == showText { return }
element.showText = showText
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *ToggleButton) HandleThemeChange () {
element.drawer.SetFace (element.entity.Theme().FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal, toggleButtonCase))
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *ToggleButton) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *ToggleButton) HandleMouseDown (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if !element.Enabled() { return }
element.Focus()
if button != input.ButtonLeft { return }
element.pressed = true
element.entity.Invalidate()
}
func (element *ToggleButton) HandleMouseUp (
position image.Point,
button input.Button,
modifiers input.Modifiers,
) {
if button != input.ButtonLeft { return }
element.pressed = false
within := position.In(element.entity.Bounds())
if element.Enabled() && within {
element.on = !element.on
if element.onToggle != nil {
element.onToggle()
}
}
element.entity.Invalidate()
}
func (element *ToggleButton) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *ToggleButton) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.entity.Invalidate()
if !element.Enabled() { return }
element.on = !element.on
if element.onToggle != nil {
element.onToggle()
}
}
}
func (element *ToggleButton) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternButton, toggleButtonCase)
margin := element.entity.Theme().Margin(tomo.PatternButton, toggleButtonCase)
lampPadding := element.entity.Theme().Padding(tomo.PatternLamp, toggleButtonCase)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
if element.hasIcon {
icon := element.entity.Theme().Icon(element.iconId, tomo.IconSizeSmall, toggleButtonCase)
if icon != nil {
bounds := icon.Bounds()
if element.showText {
minimumSize.Max.X += bounds.Dx()
minimumSize.Max.X += margin.X
} else {
minimumSize.Max.X = bounds.Dx()
}
}
}
minimumSize.Max.X += lampPadding.Horizontal()
minimumSize = padding.Inverse().Apply(minimumSize)
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
}
func (element *ToggleButton) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
On: element.on,
}
}

View File

@ -1,39 +0,0 @@
package elements
import "image"
// Window represents a top-level container generated by the currently running
// backend. It can contain a single element. It is hidden by default, and must
// be explicitly shown with the Show() method. If it contains no element, it
// displays a black (or transprent) background.
type Window interface {
// Adopt sets the root element of the window. There can only be one of
// these at one time.
Adopt (child Element)
// Child returns the root element of the window.
Child () (child Element)
// SetTitle sets the title that appears on the window's title bar. This
// method might have no effect with some backends.
SetTitle (title string)
// SetIcon taks in a list different sizes of the same icon and selects
// the best one to display on the window title bar, dock, or whatever is
// applicable for the given backend. This method might have no effect
// for some backends.
SetIcon (sizes []image.Image)
// Show shows the window. The window starts off hidden, so this must be
// called after initial setup to make sure it is visible.
Show ()
// Hide hides the window.
Hide ()
// Close closes the window.
Close ()
// OnClose specifies a function to be called when the window is closed.
OnClose (func ())
}

114
entity.go Normal file
View File

@ -0,0 +1,114 @@
package tomo
import "image"
import "art"
// Entity is a handle given to elements by the backend. Extended entity
// interfaces are defined in the ability module.
type Entity interface {
// Invalidate marks the element's current visual as invalid. At the end
// of every event, the backend will ask all invalid entities to redraw
// themselves.
Invalidate ()
// InvalidateLayout marks the element's layout as invalid. At the end of
// every event, the backend will ask all invalid elements to recalculate
// their layouts.
InvalidateLayout ()
// Bounds returns the bounds of the element to be used for drawing and
// layout.
Bounds () image.Rectangle
// Window returns the window that the element is in.
Window () Window
// SetMinimumSize reports to the system what the element's minimum size
// can be. The minimum size of child elements should be taken into
// account when calculating this.
SetMinimumSize (width, height int)
// DrawBackground asks the parent element to draw its background pattern
// to a canvas. This should be used for transparent elements like text
// labels. If there is no parent element (that is, the element is
// directly inside of the window), the backend will draw a default
// background pattern.
DrawBackground (art.Canvas)
// --- Behaviors relating to parenting ---
// Adopt adds an element as a child.
Adopt (child Element)
// Insert inserts an element in the child list at the specified
// location.
Insert (index int, child Element)
// Disown removes the child at the specified index.
Disown (index int)
// IndexOf returns the index of the specified child.
IndexOf (child Element) int
// Child returns the child at the specified index.
Child (index int) Element
// CountChildren returns the amount of children the element has.
CountChildren () int
// PlaceChild sets the size and position of the child at the specified
// index to a bounding rectangle.
PlaceChild (index int, bounds image.Rectangle)
// SelectChild marks a child as selected or unselected, if it is
// selectable.
SelectChild (index int, selected bool)
// ChildMinimumSize returns the minimum size of the child at the
// specified index.
ChildMinimumSize (index int) (width, height int)
// --- Behaviors relating to input focus ---
// Focused returns whether the element currently has input focus.
Focused () bool
// Focus sets this element as focused. If this succeeds, the element will
// recieve a HandleFocus call.
Focus ()
// FocusNext causes the focus to move to the next element. If this
// succeeds, the element will recieve a HandleUnfocus call.
FocusNext ()
// FocusPrevious causes the focus to move to the next element. If this
// succeeds, the element will recieve a HandleUnfocus call.
FocusPrevious ()
// Selected returns whether this element is currently selected.
Selected () bool
// --- Behaviors relating to scrolling --- //
// NotifyFlexibleHeightChange notifies the system that the parameters
// affecting the element's flexible height have changed. This method is
// expected to be called by flexible elements when their content changes.
NotifyFlexibleHeightChange ()
// NotifyScrollBoundsChange notifies the system that the element's
// scroll content bounds or viewport bounds have changed. This is
// expected to be called by scrollable elements when they change their
// supported scroll axes, their scroll position (either autonomously or
// as a result of a call to ScrollTo()), or their content size.
NotifyScrollBoundsChange ()
// --- Behaviors relating to themeing ---
// Theme returns the currently active theme. When this value changes,
// the HandleThemeChange method of the element is called.
Theme () Theme
// Config returns the currently active config. When this value changes,
// the HandleThemeChange method of the element is called.
Config () Config
}

View File

@ -1,22 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
import _ "net/http/pprof"
import "net/http"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(480, 360)
window.SetTitle("Draw Test")
window.Adopt(testing.NewArtist())
window.OnClose(tomo.Stop)
window.Show()
go func () {
http.ListenAndServe("localhost:9090", nil)
} ()
}

View File

@ -1,29 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window.SetTitle("example button")
button := basicElements.NewButton("hello tomo!")
button.OnClick (func () {
// when we set the button's text to something longer, the window
// will automatically resize to accomodate it.
button.SetText("you clicked me.\nwow, there are two lines!")
button.OnClick (func () {
button.SetText (
"stop clicking me you idiot!\n" +
"you've already seen it all!")
button.OnClick(tomo.Stop)
})
})
window.Adopt(button)
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,49 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window.SetTitle("Checkboxes")
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container)
container.Adopt (basicElements.NewLabel (
"We advise you to not read thPlease listen to me. I am " +
"trapped inside the example code. This is the only way for " +
"me to communicate.", true), true)
container.Adopt(basicElements.NewSpacer(true), false)
container.Adopt(basicElements.NewCheckbox("Oh god", false), false)
container.Adopt(basicElements.NewCheckbox("Can you hear them", true), false)
container.Adopt(basicElements.NewCheckbox("They are in the walls", false), false)
container.Adopt(basicElements.NewCheckbox("They are coming for us", false), false)
disabledCheckbox := basicElements.NewCheckbox("We are but their helpless prey", false)
disabledCheckbox.SetEnabled(false)
container.Adopt(disabledCheckbox, false)
vsync := basicElements.NewCheckbox("Enable vsync", false)
vsync.OnToggle (func () {
if vsync.Value() {
popups.NewDialog (
popups.DialogKindInfo,
"Ha!",
"That doesn't do anything.")
}
})
container.Adopt(vsync, false)
button := basicElements.NewButton("What")
button.OnClick(tomo.Stop)
container.Adopt(button, false)
button.Focus()
window.OnClose(tomo.Stop)
window.Show()
}

139
examples/clipboard/main.go Normal file
View File

@ -0,0 +1,139 @@
package main
import "io"
import "image"
import _ "image/png"
import _ "image/gif"
import _ "image/jpeg"
import "tomo"
import "tomo/data"
import "tomo/nasin"
import "tomo/popups"
import "tomo/elements"
var validImageTypes = []data.Mime {
data.M("image", "png"),
data.M("image", "gif"),
data.M("image", "jpeg"),
}
func main () {
nasin.Run(Application { })
}
type Application struct { }
func (Application) Init () error {
window, err:= nasin.NewWindow(tomo.Bounds(0, 0, 256, 0))
if err != nil { return err }
window.SetTitle("Clipboard")
container := elements.NewVBox(elements.SpaceBoth)
textInput := elements.NewTextBox("", "")
controlRow := elements.NewHBox(elements.SpaceMargin)
copyButton := elements.NewButton("Copy")
copyButton.SetIcon(tomo.IconCopy)
pasteButton := elements.NewButton("Paste")
pasteButton.SetIcon(tomo.IconPaste)
pasteImageButton := elements.NewButton("Image")
pasteImageButton.SetIcon(tomo.IconPictures)
imageClipboardCallback := func (clipboard data.Data, err error) {
if err != nil {
popups.NewDialog (
popups.DialogKindError,
window,
"Error",
"Cannot get clipboard:\n" + err.Error())
return
}
var imageData io.Reader
var ok bool
for mime, reader := range clipboard {
for _, mimeCheck := range validImageTypes {
if mime == mimeCheck {
imageData = reader
ok = true
}}}
if !ok {
popups.NewDialog (
popups.DialogKindError,
window,
"Clipboard Empty",
"No image data in clipboard")
return
}
img, _, err := image.Decode(imageData)
if err != nil {
popups.NewDialog (
popups.DialogKindError,
window,
"Error",
"Cannot decode image:\n" + err.Error())
return
}
imageWindow(window, img)
}
clipboardCallback := func (clipboard data.Data, err error) {
if err != nil {
popups.NewDialog (
popups.DialogKindError,
window,
"Error",
"Cannot get clipboard:\n" + err.Error())
return
}
textData, ok := clipboard[data.MimePlain]
if !ok {
popups.NewDialog (
popups.DialogKindError,
window,
"Clipboard Empty",
"No text data in clipboard")
return
}
text, _ := io.ReadAll(textData)
textInput.SetValue(string(text))
}
copyButton.OnClick (func () {
window.Copy(data.Text(textInput.Value()))
})
pasteButton.OnClick (func () {
window.Paste(clipboardCallback, data.MimePlain)
})
pasteImageButton.OnClick (func () {
window.Paste(imageClipboardCallback, validImageTypes...)
})
container.AdoptExpand(textInput)
controlRow.AdoptExpand(copyButton)
controlRow.AdoptExpand(pasteButton)
controlRow.AdoptExpand(pasteImageButton)
container.Adopt(controlRow)
window.Adopt(container)
window.OnClose(nasin.Stop)
window.Show()
return nil
}
func imageWindow (parent tomo.Window, image image.Image) {
window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Clipboard Image")
container := elements.NewVBox(elements.SpaceBoth)
closeButton := elements.NewButton("Ok")
closeButton.SetIcon(tomo.IconYes)
closeButton.OnClick(window.Close)
container.AdoptExpand(elements.NewImage(image))
container.Adopt(closeButton)
window.Adopt(container)
closeButton.Focus()
window.Show()
}

View File

@ -1,29 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window.SetTitle("dialog")
container := basicElements.NewContainer(basicLayouts.Dialog { true, true })
window.Adopt(container)
container.Adopt(basicElements.NewLabel("you will explode", true), true)
cancel := basicElements.NewButton("Cancel")
cancel.SetEnabled(false)
container.Adopt(cancel, false)
okButton := basicElements.NewButton("OK")
container.Adopt(okButton, false)
okButton.Focus()
window.OnClose(tomo.Stop)
window.Show()
}

65
examples/document/main.go Normal file
View File

@ -0,0 +1,65 @@
package main
import "os"
import "image"
import _ "image/png"
import "tomo"
import "tomo/nasin"
import "tomo/elements"
func main () {
nasin.Run(Application { })
}
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 383, 360))
if err != nil { return err }
window.SetTitle("Document Container")
file, err := os.Open("assets/banner.png")
if err != nil { return err }
logo, _, err := image.Decode(file)
file.Close()
if err != nil { return err }
document := elements.NewDocument()
document.Adopt (
elements.NewLabelWrapped (
"A document container is a vertically stacked container " +
"capable of properly laying out flexible elements such as " +
"text-wrapped labels. You can also include normal elements " +
"like:"),
elements.NewButton("Buttons,"),
elements.NewCheckbox("Checkboxes,", true),
elements.NewTextBox("", "And text boxes."),
elements.NewLine(),
elements.NewLabelWrapped (
"Document containers are meant to be placed inside of a " +
"ScrollContainer, like this one."),
elements.NewLabelWrapped (
"You could use document containers to do things like display various " +
"forms of hypertext (like HTML, gemtext, markdown, etc.), " +
"lay out a settings menu with descriptive label text between " +
"control groups like in iOS, or list comment or chat histories."),
elements.NewImage(logo),
elements.NewLabelWrapped (
"You can also choose whether each element is on its own line " +
"(sort of like an HTML/CSS block element) or on a line with " +
"other adjacent elements (like an HTML/CSS inline element)."))
document.AdoptInline (
elements.NewButton("Just"),
elements.NewButton("like"),
elements.NewButton("this."))
document.Adopt (elements.NewLabelWrapped (
"Oh, you're a switch? Then name all of these switches:"))
for i := 0; i < 30; i ++ {
document.AdoptInline(elements.NewSwitch("", false))
}
window.Adopt(elements.NewScroll(elements.ScrollVertical, document))
window.OnClose(nasin.Stop)
window.Show()
return nil
}

22
examples/drawing/main.go Normal file
View File

@ -0,0 +1,22 @@
package main
import "tomo"
import "tomo/nasin"
import "tomo/elements/testing"
import "git.tebibyte.media/sashakoshka/ezprof/ez"
func main () {
nasin.Run(Application { })
}
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 480, 360))
if err != nil { return err }
window.Adopt(testing.NewArtist())
window.OnClose(nasin.Stop)
window.Show()
ez.Prof()
return nil
}

View File

@ -0,0 +1,88 @@
package main
import "os"
import "path/filepath"
import "tomo"
import "tomo/nasin"
import "tomo/elements"
func main () {
nasin.Run(Application { })
}
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 384, 384))
if err != nil { return err }
window.SetTitle("File browser")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
homeDir, err := os.UserHomeDir()
if err != nil { return err }
controlBar := elements.NewHBox(elements.SpaceNone)
backButton := elements.NewButton("Back")
backButton.SetIcon(tomo.IconBackward)
backButton.ShowText(false)
forwardButton := elements.NewButton("Forward")
forwardButton.SetIcon(tomo.IconForward)
forwardButton.ShowText(false)
refreshButton := elements.NewButton("Refresh")
refreshButton.SetIcon(tomo.IconRefresh)
refreshButton.ShowText(false)
upwardButton := elements.NewButton("Go Up")
upwardButton.SetIcon(tomo.IconUpward)
upwardButton.ShowText(false)
locationInput := elements.NewTextBox("Location", "")
statusBar := elements.NewHBox(elements.SpaceMargin)
directory, _ := elements.NewFile(homeDir, nil)
baseName := elements.NewLabel(filepath.Base(homeDir))
directoryView, _ := elements.NewDirectory(homeDir, nil)
updateStatus := func () {
filePath, _ := directoryView.Location()
directory.SetLocation(filePath, nil)
locationInput.SetValue(filePath)
baseName.SetText(filepath.Base(filePath))
}
choose := func (filePath string) {
directoryView.SetLocation(filePath, nil)
updateStatus()
}
directoryView.OnChoose(choose)
locationInput.OnEnter (func () {
choose(locationInput.Value())
})
choose(homeDir)
backButton.OnClick (func () {
directoryView.Backward()
updateStatus()
})
forwardButton.OnClick (func () {
directoryView.Forward()
updateStatus()
})
refreshButton.OnClick (func () {
directoryView.Update()
updateStatus()
})
upwardButton.OnClick (func () {
filePath, _ := directoryView.Location()
choose(filepath.Dir(filePath))
})
controlBar.Adopt(backButton, forwardButton, refreshButton, upwardButton)
controlBar.AdoptExpand(locationInput)
statusBar.Adopt(directory, baseName)
container.Adopt(controlBar)
container.AdoptExpand (
elements.NewScroll(elements.ScrollVertical, directoryView))
container.Adopt(statusBar)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

View File

@ -1,114 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/flow"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window.SetTitle("adventure")
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.Adopt(container)
var world flow.Flow
world.Transition = container.DisownAll
world.Stages = map [string] func () {
"start": func () {
label := basicElements.NewLabel (
"you are standing next to a river.", true)
button0 := basicElements.NewButton("go in the river")
button0.OnClick(world.SwitchFunc("wet"))
button1 := basicElements.NewButton("walk along the river")
button1.OnClick(world.SwitchFunc("house"))
button2 := basicElements.NewButton("turn around")
button2.OnClick(world.SwitchFunc("bear"))
container.Warp ( func () {
container.Adopt(label, true)
container.Adopt(button0, false)
container.Adopt(button1, false)
container.Adopt(button2, false)
button0.Focus()
})
},
"wet": func () {
label := basicElements.NewLabel (
"you get completely soaked.\n" +
"you die of hypothermia.", true)
button0 := basicElements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := basicElements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button0, false)
container.Adopt(button1, false)
button0.Focus()
})
},
"house": func () {
label := basicElements.NewLabel (
"you are standing in front of a delapidated " +
"house.", true)
button1 := basicElements.NewButton("go inside")
button1.OnClick(world.SwitchFunc("inside"))
button0 := basicElements.NewButton("turn back")
button0.OnClick(world.SwitchFunc("start"))
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button1, false)
container.Adopt(button0, false)
button1.Focus()
})
},
"inside": func () {
label := basicElements.NewLabel (
"you are standing inside of the house.\n" +
"it is dark, but rays of light stream " +
"through the window.\n" +
"there is nothing particularly interesting " +
"here.", true)
button0 := basicElements.NewButton("go back outside")
button0.OnClick(world.SwitchFunc("house"))
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button0, false)
button0.Focus()
})
},
"bear": func () {
label := basicElements.NewLabel (
"you come face to face with a bear.\n" +
"it eats you (it was hungry).", true)
button0 := basicElements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := basicElements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button0, false)
container.Adopt(button1, false)
button0.Focus()
})
},
}
world.Switch("start")
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,41 +1,43 @@
package main
import "os"
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
import "tomo"
import "tomo/nasin"
import "tomo/elements"
import "tomo/elements/fun"
func main () {
tomo.Run(run)
os.Exit(0)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(2, 2)
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 200, 216))
if err != nil { return err }
window.SetTitle("Clock")
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
window.SetApplicationName("TomoClock")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
clock := fun.NewAnalogClock(time.Now())
container.Adopt(clock, true)
label := basicElements.NewLabel(formatTime(), false)
container.Adopt(label, false)
label := elements.NewLabel(formatTime())
container.AdoptExpand(clock)
container.Adopt(label)
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
go tick(label, clock)
return nil
}
func formatTime () (timeString string) {
return time.Now().Format("2006-01-02 15:04:05")
}
func tick (label *basicElements.Label, clock *fun.AnalogClock) {
func tick (label *elements.Label, clock *fun.AnalogClock) {
for {
tomo.Do (func () {
nasin.Do (func () {
label.SetText(formatTime())
clock.SetTime(time.Now())
})

View File

@ -1,25 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(360, 2)
window.SetTitle("horizontal stack")
container := basicElements.NewContainer(basicLayouts.Horizontal { true, true })
window.Adopt(container)
container.Adopt(basicElements.NewLabel("this is sample text", true), true)
container.Adopt(basicElements.NewLabel("this is sample text", true), true)
container.Adopt(basicElements.NewLabel("this is sample text", true), true)
window.OnClose(tomo.Stop)
window.Show()
}

46
examples/icons/main.go Normal file
View File

@ -0,0 +1,46 @@
package main
import "tomo"
import "tomo/nasin"
import "tomo/elements"
func main () {
nasin.Run(Application { })
}
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 360, 0))
if err != nil { return err }
window.SetTitle("Icons")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
container.Adopt (
elements.NewLabel("Just some of the wonderful icons we have:"),
elements.NewLine())
container.AdoptExpand (
icons(tomo.IconHome, tomo.IconHistory),
icons(tomo.IconFile, tomo.IconNetwork),
icons(tomo.IconOpen, tomo.IconRemoveFavorite),
icons(tomo.IconCursor, tomo.IconDistort))
closeButton := elements.NewButton("Yes verynice")
closeButton.SetIcon(tomo.IconYes)
closeButton.OnClick(window.Close)
container.Adopt(closeButton)
window.OnClose(nasin.Stop)
window.Show()
return nil
}
func icons (min, max tomo.Icon) (container *elements.Box) {
container = elements.NewHBox(elements.SpaceMargin)
for index := min; index <= max; index ++ {
container.AdoptExpand(elements.NewIcon(index, tomo.IconSizeSmall))
}
return
}

View File

@ -5,54 +5,59 @@ import "image"
import "bytes"
import _ "image/png"
import "github.com/jezek/xgbutil/gopher"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
import "tomo"
import "tomo/nasin"
import "tomo/popups"
import "tomo/elements"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(2, 2)
type Application struct { }
func (Application) Init () error {
window, _ := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Tomo Logo")
file, err := os.Open("assets/banner.png")
if err != nil { fatalError(err); return }
if err != nil { return err }
logo, _, err := image.Decode(file)
file.Close()
if err != nil { fatalError(err); return }
if err != nil { return err }
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
logoImage := basicElements.NewImage(logo)
button := basicElements.NewButton("Show me a gopher instead")
button.OnClick (func () { container.Warp (func () {
container.DisownAll()
gopher, _, err :=
image.Decode(bytes.NewReader(gopher.GopherPng()))
if err != nil { fatalError(err); return }
container.Adopt(basicElements.NewImage(gopher),true)
}) })
container := elements.NewVBox(elements.SpaceBoth)
logoImage := elements.NewImage(logo)
button := elements.NewButton("Show me a gopher instead")
button.OnClick (func () {
window.SetTitle("Not the Tomo Logo")
container.DisownAll()
gopher, _, err :=
image.Decode(bytes.NewReader(gopher.GopherPng()))
if err != nil { fatalError(window, err); return }
container.AdoptExpand(elements.NewImage(gopher))
})
container.Adopt(logoImage, true)
container.Adopt(button, false)
container.AdoptExpand(logoImage)
container.Adopt(button)
window.Adopt(container)
button.Focus()
window.OnClose(tomo.Stop)
window.OnClose(nasin.Stop)
window.Show()
return nil
}
func fatalError (err error) {
func fatalError (window tomo.Window, err error) {
popups.NewDialog (
popups.DialogKindError,
window,
"Error",
err.Error(),
popups.Button {
Name: "OK",
OnPress: tomo.Stop,
OnPress: nasin.Stop,
})
}

View File

@ -1,32 +1,40 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
import "tomo"
import "tomo/nasin"
import "tomo/popups"
import "tomo/elements"
func main () {
tomo.Run(run)
nasin.Run(Application { })
}
func run () {
window, _ := tomo.NewWindow(2, 2)
type Application struct { }
func (Application) Init () error {
window, err := nasin.NewWindow(tomo.Bounds(0, 0, 0, 0))
if err != nil { return err }
window.SetTitle("Enter Details")
container := basicElements.NewContainer(basicLayouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
// create inputs
firstName := basicElements.NewTextBox("First name", "")
lastName := basicElements.NewTextBox("Last name", "")
fingerLength := basicElements.NewTextBox("Length of fingers", "")
button := basicElements.NewButton("Ok")
firstName := elements.NewTextBox("First name", "")
lastName := elements.NewTextBox("Last name", "")
fingerLength := elements.NewTextBox("Length of fingers", "")
purpose := elements.NewComboBox (
"",
"Gaslight",
"Gatekeep",
"Girlboss")
button := elements.NewButton("Ok")
button.SetEnabled(false)
button.OnClick (func () {
// create a dialog displaying the results
popups.NewDialog (
popups.DialogKindInfo,
window,
"Profile",
firstName.Value() + " " + lastName.Value() +
"'s fingers\nmeasure in at " + fingerLength.Value() +
@ -36,22 +44,25 @@ func run () {
// enable the Ok button if all three inputs have text in them
check := func () {
button.SetEnabled (
firstName.Filled() &&
lastName.Filled() &&
fingerLength.Filled())
firstName.Filled() &&
lastName.Filled() &&
fingerLength.Filled() &&
purpose.Filled())
}
firstName.OnChange(check)
lastName.OnChange(check)
fingerLength.OnChange(check)
purpose.OnChange(check)
// add elements to container
container.Adopt(basicElements.NewLabel("Choose your words carefully.", false), true)
container.Adopt(firstName, false)
container.Adopt(lastName, false)
container.Adopt(fingerLength, false)
container.Adopt(basicElements.NewSpacer(true), false)
container.Adopt(button, false)
window.OnClose(tomo.Stop)
container.AdoptExpand(elements.NewLabel("Choose your words carefully."))
container.Adopt (
firstName, lastName,
fingerLength,
elements.NewLabel("Purpose:"),
purpose,
elements.NewLine(), button)
window.OnClose(nasin.Stop)
window.Show()
return nil
}

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