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.

433 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
Sasha Koshka 1f2e8aa677 Some final theme tweaks 2023-03-01 13:06:34 -05:00
Sasha Koshka ef59f46559 Made the progress bar look nicer 2023-02-28 19:00:34 -05:00
Sasha Koshka 829f1525b8 Not even gonna bother writing a good name 2023-02-28 17:15:20 -05:00
Sasha Koshka b1d15fb4ec this piano is DOPE and PHAT and WAY COOL 2023-02-28 00:17:05 -05:00
Sasha Koshka ee45b2fa60 Theming tweaks and rendering fixes 2023-02-27 17:00:28 -05:00
Sasha Koshka 8dd506a007 Textures now render properly 2023-02-27 16:38:33 -05:00
Sasha Koshka de10cde630 Add image textures to theme 2023-02-27 12:48:44 -05:00
Sasha Koshka 449922851f Fix list not drawing background 2023-02-26 22:56:20 -05:00
Sasha Koshka 26787d8670 Fixed TextBox 2023-02-26 22:48:14 -05:00
Sasha Koshka cda2d1f0ae Default elements compile 2023-02-26 22:20:17 -05:00
Sasha Koshka 241c297626 whee back in busineess 2023-02-26 14:27:38 -05:00
Sasha Koshka 2859dc3313 Cleaned out the old theme code and moved padding and margins to theme 2023-02-26 00:44:44 -05:00
Sasha Koshka 7e51dc5e5a Documented artist package 2023-02-25 23:04:51 -05:00
Sasha Koshka 81090267a6 Created new patterns 2023-02-25 18:41:16 -05:00
Sasha Koshka bf2fdb5eaa Ellipse and rectangle have both color and source routines 2023-02-24 16:31:42 -05:00
Sasha Koshka 211219eb01 Ellipse and line share code 2023-02-24 02:51:24 -05:00
Sasha Koshka 79ab1c8ac0 Existing shape routines have been reimplemented 2023-02-24 02:26:34 -05:00
Sasha Koshka d167559830 Got rectangles all sorted 2023-02-23 20:55:19 -05:00
Sasha Koshka 48237f5687 Add AllocateSample 2023-02-23 17:44:53 -05:00
Sasha Koshka 0ba3c982c4 Added some utility functions to pattern 2023-02-23 15:00:44 -05:00
Sasha Koshka c7e44633b1 Updated Pattern interface 2023-02-23 14:44:54 -05:00
Sasha Koshka b575413a0a For later 2023-02-23 12:12:25 -05:00
Sasha Koshka 29e4a7572b Added health and stamina 2023-02-21 18:53:19 -05:00
Sasha Koshka ddb960571f Fixed texture warping when too close to walls 2023-02-21 18:15:41 -05:00
Sasha Koshka ce1d938f7a Fixed the wierd wall overlap 2023-02-21 17:57:52 -05:00
Sasha Koshka 20fa445cdd backrooms!!!!! 2023-02-21 16:48:56 -05:00
Sasha Koshka e966771f5b The raycaster is faster but more bg=uggyh agghgfghgfhgfgh 2023-02-21 13:30:32 -05:00
Sasha Koshka e9e1ccc35e Added basic raycaster demo. I have no idea why I did this. 2023-02-20 01:52:50 -05:00
Sasha Koshka 0c39c2dd57 Button takes advantage of the rendering hints 2023-02-16 22:41:07 -05:00
Sasha Koshka f8240fb518 Created FillRectangleShatter for convenience 2023-02-16 18:19:36 -05:00
Sasha Koshka fc0a9292d9 Added rendering optimization hints to themes 2023-02-16 18:00:15 -05:00
Sasha Koshka b9cbf83a18 Added the collapse behavior as an interface 2023-02-16 17:35:53 -05:00
Sasha Koshka 270b49f825 Removed that annoying log message on window close 2023-02-16 17:22:33 -05:00
Sasha Koshka e3369ab3d4 AAAAART! 2023-02-16 14:57:46 -05:00
Sasha Koshka 50e9c3b1c9 The null rune at the end is fake now 2023-02-16 14:43:36 -05:00
Sasha Koshka e2e846a0e5 AHHHHH!!! 2023-02-16 14:39:51 -05:00
Sasha Koshka fa934fa485 Keyboard text selection is now no longer broken lmao 2023-02-16 14:09:23 -05:00
Sasha Koshka 56dc9ba54c this just j 2023-02-16 12:35:31 -05:00
Sasha Koshka 7235c86e22 TypeSetter properly adds a null char onto the end of its text 2023-02-16 02:22:32 -05:00
Sasha Koshka 367aee4570 Improved accuracy of TypeSetter again 2023-02-16 01:55:00 -05:00
Sasha Koshka bd55b6c17d Improved accuracy of type setter 2023-02-15 20:16:49 -05:00
Sasha Koshka a0e7bf1373 Integrated the new text drawer 2023-02-15 18:45:58 -05:00
Sasha Koshka 234503f104 Added fixed precision point utilities 2023-02-15 18:41:03 -05:00
Sasha Koshka ae551c47ea Replace TextDrawer with more capable system 2023-02-15 18:17:17 -05:00
Sasha Koshka 0c22977693 TextDrawer does not separate whitespace from printables 2023-02-14 18:11:11 -05:00
Sasha Koshka 4d87972235 Hot themeing tweaks 2023-02-14 17:21:05 -05:00
Sasha Koshka d59b7d812d Stop some redundant rendering in the piano 2023-02-14 17:05:13 -05:00
Sasha Koshka 09f782953e Use FillRectangleClip in List and Container 2023-02-14 16:53:28 -05:00
Sasha Koshka fa42cf1f5f Added a new FillRectangleClip function 2023-02-14 15:47:41 -05:00
Sasha Koshka dcaf9919e4 Fix thos issue 2023-02-14 02:14:52 -05:00
Sasha Koshka d18da8b07a Rudimentary text selection with the mouse 2023-02-13 18:29:49 -05:00
Sasha Koshka 88502cf628 Improved keyboard selection somewhat 2023-02-13 15:26:21 -05:00
Sasha Koshka 21abd147bf Rudimentary text selection with keybaord keys 2023-02-13 12:55:51 -05:00
Sasha Koshka 4bc8566820 Textmanip now operates on a dot instead of a cursor 2023-02-13 01:52:31 -05:00
Sasha Koshka 8ac5108211 Elements are no longer images 2023-02-13 01:49:33 -05:00
Sasha Koshka 7f0462d588 Changed the order of the Theme.Pattern method 2023-02-12 10:58:23 -05:00
Sasha Koshka 82e92f1e2e Icons are now no longer patterns, they are images 2023-02-12 10:55:32 -05:00
Sasha Koshka 9e8e986977 Changes to how scroll bars respond to the mouse
- Left clicking on the gutter jumps to that position
- Right clicking on the gutter scrolls incrementally towards that
  position
- Middle clicking on the gutter pages up or down to that position
2023-02-11 22:17:03 -05:00
Sasha Koshka 2d9a941da8 Lists no longer have stale scroll values when enlarged 2023-02-11 21:45:04 -05:00
Sasha Koshka c64ce8da67 Container shatters its background before drawing 2023-02-11 21:17:43 -05:00
Sasha Koshka a893831a21 Added a shatter function to subtract rectangles from a rectangle 2023-02-11 21:07:35 -05:00
Sasha Koshka 7f1c3ae870 Added documentation for the sliders 2023-02-11 17:04:50 -05:00
Sasha Koshka d7a6193c04 Added gain slider 2023-02-11 01:46:12 -05:00
Sasha Koshka a74f9809af Awesome labels 2023-02-11 01:27:28 -05:00
Sasha Koshka f9032a9a95 Oh yeah babey 2023-02-11 01:06:47 -05:00
Sasha Koshka 0e3de11203 Fixed a focus issue with ScrollContainer 2023-02-11 00:58:54 -05:00
Sasha Koshka 981c11bd44 Fixed the list widget 2023-02-11 00:18:21 -05:00
Sasha Koshka dce0321e9b Added a Select() method to List 2023-02-10 22:26:34 -05:00
Sasha Koshka 5e448edb21 Added sliders and made the ADSR controllabe with them 2023-02-10 21:55:59 -05:00
Sasha Koshka c33faa402b Made the supersaw a bit better and actually a supersaw 2023-02-10 15:08:20 -05:00
Sasha Koshka 182cb1e35b The piano now has an internal ADSR 2023-02-09 23:52:27 -05:00
Sasha Koshka cfc2b5e130 Image element for showing images 2023-02-09 18:34:53 -05:00
Sasha Koshka 6e7cf285cc Fixed issue with X backend not recognizing key repeats 2023-02-09 17:26:36 -05:00
Sasha Koshka e3aea7fc9e Better piano keybinds 2023-02-09 16:36:38 -05:00
Sasha Koshka 5446ffe40b h a r m o n y 2023-02-09 16:15:02 -05:00
Sasha Koshka 06e97461fa Note.Octave returns an Octave 2023-02-09 15:06:41 -05:00
Sasha Koshka b38232ee24 More documentation! 2023-02-09 15:05:13 -05:00
Sasha Koshka 2cd670f4cd Improved element documentation 2023-02-09 14:50:24 -05:00
Sasha Koshka c7bebabed5 Fixed issue where containers would not select themselves prperly 2023-02-09 14:25:55 -05:00
Sasha Koshka b15c260dfc Improved piano styling 2023-02-09 11:38:01 -05:00
Sasha Koshka 16ce15621e Moar waveforms!!!! 2023-02-09 02:04:58 -05:00
Sasha Koshka 16a0e76145 Removed a bunch of redundant draw calls
Most were related to a but with the keynav api
2023-02-09 01:30:14 -05:00
Sasha Koshka ce20b7d02c Piano can now play square waves 2023-02-09 00:01:39 -05:00
Sasha Koshka c5ee7c8cdb The piano plays sound 2023-02-08 23:41:31 -05:00
Sasha Koshka 5c7e243566 Merge pull request 'restructure-config' (#8) from restructure-config into main
Reviewed-on: sashakoshka/tomo#8
2023-02-09 02:07:08 +00:00
Sasha Koshka bec8b817c8 Added a piano widget because why not really 2023-02-08 21:05:36 -05:00
Sasha Koshka 6cc0f36000 Migrated the clock 2023-02-08 15:12:18 -05:00
Sasha Koshka a0e57921a4 Oh my jod 2023-02-08 14:36:14 -05:00
Sasha Koshka 6936353516 asuhfdjkshlk 2023-02-08 00:22:40 -05:00
Sasha Koshka 3998d842b1 Half-done migration of basic elements 2023-02-07 11:27:59 -05:00
Sasha Koshka 0bdbaa39ca Artist and test examples work 2023-02-03 18:32:22 -05:00
Sasha Koshka f8ebe5b1e4 Core provides convenience methods for easy theme access 2023-02-03 18:28:01 -05:00
Sasha Koshka 8d90dbdc92 Element core now deals with Config and Theme objects 2023-02-03 18:07:10 -05:00
Sasha Koshka 43fea5c8ba Tomo will call the parse functions in Theme and Config 2023-02-03 17:50:45 -05:00
Sasha Koshka 2ff32ca8ea Added thing to get standard directories 2023-02-03 17:06:51 -05:00
Sasha Koshka d79701d01b X backend conforms to new API 2023-02-03 01:35:59 -05:00
Sasha Koshka bdf599f93c Backends must now accept Config and Theme 2023-02-03 01:25:45 -05:00
Sasha Koshka 8ccaa0faba Added Themeable and Configurable element interfaces 2023-02-03 01:14:03 -05:00
Sasha Koshka 83b8040520 Theme stub 2023-02-03 00:57:18 -05:00
Sasha Koshka 4722656c7d Config stub 2023-02-02 18:20:02 -05:00
Sasha Koshka 14d1836209 Merge branch 'main' of git.tebibyte.media:sashakoshka/tomo 2023-02-02 17:58:51 -05:00
Sasha Koshka 36b995c514 Added link to github mirror 2023-02-02 17:57:56 -05:00
Sasha Koshka 46574cfb10 Merge pull request 'atomize-modules' (#7) from atomize-modules into main
Reviewed-on: sashakoshka/tomo#7
2023-02-02 22:51:23 +00:00
Sasha Koshka 8606968c74 Separate config and theme 2023-02-02 15:19:56 -05:00
Sasha Koshka 892c74a9da Updated everything else to match 2023-02-02 01:48:38 -05:00
Sasha Koshka 99942466f8 Updated X backend to match 2023-02-02 01:47:55 -05:00
Sasha Koshka da6fe2c845 Updated layouts to match 2023-02-02 01:47:31 -05:00
Sasha Koshka 04d2ea4767 Atomized the functionality of the base tomo package 2023-02-02 01:47:01 -05:00
Sasha Koshka f71f789b60 BasicCanvas.Reallocate refuses to work on cut canvases 2023-02-01 01:52:50 -05:00
Sasha Koshka 8f0f2be9e9 Reduce allocation of X buffers and canvases 2023-02-01 01:47:08 -05:00
162 changed files with 11108 additions and 7306 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -1,27 +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,46 +0,0 @@
package artist
import "image/color"
// Beveled is a pattern that has a highlight section and a shadow section.
type Beveled [2]Pattern
// AtWhen satisfies the Pattern interface.
func (pattern Beveled) AtWhen (x, y, width, height int) (c color.RGBA) {
return QuadBeveled {
pattern[0],
pattern[1],
pattern[1],
pattern[0],
}.AtWhen(x, y, width, height)
}
// QuadBeveled is like Beveled, but with four sides. A pattern can be specified
// for each one.
type QuadBeveled [4]Pattern
// AtWhen satisfies the Pattern interface.
func (pattern QuadBeveled) AtWhen (x, y, width, height int) (c color.RGBA) {
bottom := y > height / 2
right := x > width / 2
top := !bottom
left := !right
side := 0
switch {
case top && left:
if x < y { side = 3 } else { side = 0 }
case top && right:
if width - x > y { side = 0 } else { side = 1 }
case bottom && left:
if x < height - y { side = 3 } else { side = 2 }
case bottom && right:
if width - x > height - y { side = 2 } else { side = 1 }
}
return pattern[side].AtWhen(x, y, width, height)
}

View File

@ -1,111 +0,0 @@
package artist
import "image"
import "image/color"
// Bordered is a pattern with a border and a fill.
type Bordered struct {
Fill Pattern
Stroke
}
// AtWhen satisfies the Pattern interface.
func (pattern Bordered) AtWhen (x, y, width, height int) (c color.RGBA) {
outerBounds := image.Rectangle { Max: image.Point { width, height }}
innerBounds := outerBounds.Inset(pattern.Weight)
if (image.Point { x, y }).In (innerBounds) {
return pattern.Fill.AtWhen (
x - pattern.Weight,
y - pattern.Weight,
innerBounds.Dx(), innerBounds.Dy())
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}
// Stroke represents a stoke that has a weight and a pattern.
type Stroke struct {
Weight int
Pattern
}
type borderInternal struct {
weight int
stroke Pattern
bounds image.Rectangle
dx, dy int
}
// MultiBordered is a pattern that allows multiple borders of different lengths
// to be inset within one another. The final border is treated as a fill color,
// and its weight does not matter.
type MultiBordered struct {
borders []borderInternal
lastWidth, lastHeight int
maxBorder int
}
// NewMultiBordered creates a new MultiBordered pattern from the given list of
// borders.
func NewMultiBordered (borders ...Stroke) (multi *MultiBordered) {
internalBorders := make([]borderInternal, len(borders))
for index, border := range borders {
internalBorders[index].weight = border.Weight
internalBorders[index].stroke = border.Pattern
}
return &MultiBordered { borders: internalBorders }
}
// AtWhen satisfies the Pattern interface.
func (multi *MultiBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
if multi.lastWidth != width || multi.lastHeight != height {
multi.recalculate(width, height)
}
point := image.Point { x, y }
for index := multi.maxBorder; index >= 0; index -- {
border := multi.borders[index]
if point.In(border.bounds) {
return border.stroke.AtWhen (
point.X - border.bounds.Min.X,
point.Y - border.bounds.Min.Y,
border.dx, border.dy)
}
}
return
}
func (multi *MultiBordered) recalculate (width, height int) {
bounds := image.Rect (0, 0, width, height)
multi.maxBorder = 0
for index, border := range multi.borders {
multi.maxBorder = index
multi.borders[index].bounds = bounds
multi.borders[index].dx = bounds.Dx()
multi.borders[index].dy = bounds.Dy()
bounds = bounds.Inset(border.weight)
if bounds.Empty() { break }
}
}
// Padded is a pattern that surrounds a central fill pattern with a border that
// can have a different width for each side.
type Padded struct {
Fill Pattern
Stroke Pattern
Sides []int
}
// AtWhen satisfies the Pattern interface.
func (pattern Padded) AtWhen (x, y, width, height int) (c color.RGBA) {
innerBounds := image.Rect (
pattern.Sides[3], pattern.Sides[0],
width - pattern.Sides[1], height - pattern.Sides[2])
if (image.Point { x, y }).In (innerBounds) {
return pattern.Fill.AtWhen (
x - pattern.Sides[3],
y - pattern.Sides[0],
innerBounds.Dx(), innerBounds.Dy())
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}

View File

@ -1,51 +0,0 @@
package artist
import "image/color"
// Checkered is a pattern that produces a grid of two alternating colors.
type Checkered struct {
First Pattern
Second Pattern
CellWidth, CellHeight int
}
// AtWhen satisfies the Pattern interface.
func (pattern Checkered) AtWhen (x, y, width, height int) (c color.RGBA) {
twidth := pattern.CellWidth * 2
theight := pattern.CellHeight * 2
x %= twidth
y %= theight
if x < 0 { x += twidth }
if y < 0 { x += theight }
n := 0
if x >= pattern.CellWidth { n ++ }
if y >= pattern.CellHeight { n ++ }
x %= pattern.CellWidth
y %= pattern.CellHeight
if n % 2 == 0 {
return pattern.First.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
} else {
return pattern.Second.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
}
}
// Tiled is a pattern that tiles another pattern accross a grid.
type Tiled struct {
Pattern
CellWidth, CellHeight int
}
// AtWhen satisfies the Pattern interface.
func (pattern Tiled) AtWhen (x, y, width, height int) (c color.RGBA) {
x %= pattern.CellWidth
y %= pattern.CellHeight
if x < 0 { x += pattern.CellWidth }
if y < 0 { y += pattern.CellHeight }
return pattern.Pattern.AtWhen (
x, y, pattern.CellWidth, pattern.CellHeight)
}

View File

@ -1,33 +0,0 @@
package artist
import "math"
import "image/color"
// EllipticallyBordered is a pattern with a border and a fill that is elliptical
// in shape.
type EllipticallyBordered struct {
Fill Pattern
Stroke
}
// AtWhen satisfies the Pattern interface.
func (pattern EllipticallyBordered) AtWhen (x, y, width, height int) (c color.RGBA) {
xf := (float64(x) + 0.5) / float64(width ) * 2 - 1
yf := (float64(y) + 0.5) / float64(height) * 2 - 1
distance := math.Sqrt(xf * xf + yf * yf)
var radius float64
if width < height {
// vertical
radius = 1 - float64(pattern.Weight * 2) / float64(width)
} else {
// horizontal
radius = 1 - float64(pattern.Weight * 2) / float64(height)
}
if distance < radius {
return pattern.Fill.AtWhen(x, y, width, height)
} else {
return pattern.Stroke.AtWhen(x, y, width, height)
}
}

View File

@ -1,30 +0,0 @@
package artist
import "math"
import "image/color"
// Dotted is a pattern that produces a grid of circles.
type Dotted struct {
Background Pattern
Foreground Pattern
Size int
Spacing int
}
// AtWhen satisfies the Pattern interface.
func (pattern Dotted) AtWhen (x, y, width, height int) (c color.RGBA) {
xm := x % pattern.Spacing
ym := y % pattern.Spacing
if xm < 0 { xm += pattern.Spacing }
if ym < 0 { xm += pattern.Spacing }
radius := float64(pattern.Size) / 2
spacing := float64(pattern.Spacing) / 2 - 0.5
xf := float64(xm) - spacing
yf := float64(ym) - spacing
if math.Sqrt(xf * xf + yf * yf) > radius {
return pattern.Background.AtWhen(x, y, width, height)
} else {
return pattern.Foreground.AtWhen(x, y, width, height)
}
}

View File

@ -1,145 +0,0 @@
package artist
import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// FillEllipse draws a filled ellipse with the specified pattern.
func FillEllipse (
destination tomo.Canvas,
source Pattern,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
bounds = bounds.Canon()
data, stride := destination.Buffer()
realWidth, realHeight := bounds.Dx(), bounds.Dy()
bounds = bounds.Intersect(destination.Bounds()).Canon()
if bounds.Empty() { return }
updatedRegion = bounds
width, height := bounds.Dx(), bounds.Dy()
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
xf := (float64(x) + 0.5) / float64(realWidth) - 0.5
yf := (float64(y) + 0.5) / float64(realHeight) - 0.5
if math.Sqrt(xf * xf + yf * yf) <= 0.5 {
data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
source.AtWhen(x, y, realWidth, realHeight)
}
}}
return
}
// StrokeEllipse draws the outline of an ellipse with the specified line weight
// and pattern.
func StrokeEllipse (
destination tomo.Canvas,
source Pattern,
weight int,
bounds image.Rectangle,
) {
if weight < 1 { return }
data, stride := destination.Buffer()
bounds = bounds.Canon().Inset(weight - 1)
width, height := bounds.Dx(), bounds.Dy()
context := ellipsePlottingContext {
data: data,
stride: stride,
source: source,
width: width,
height: height,
weight: weight,
bounds: bounds,
}
bounds.Max.X -= 1
bounds.Max.Y -= 1
radii := image.Pt (
bounds.Dx() / 2,
bounds.Dy() / 2)
center := bounds.Min.Add(radii)
x := float64(0)
y := float64(radii.Y)
// region 1 decision parameter
decision1 :=
float64(radii.Y * radii.Y) -
float64(radii.X * radii.X * radii.Y) +
(0.25 * float64(radii.X) * float64(radii.X))
decisionX := float64(2 * radii.Y * radii.Y * int(x))
decisionY := float64(2 * radii.X * radii.X * int(y))
// draw region 1
for decisionX < decisionY {
context.plot( int(x) + center.X, int(y) + center.Y)
context.plot(-int(x) + center.X, int(y) + center.Y)
context.plot( int(x) + center.X, -int(y) + center.Y)
context.plot(-int(x) + center.X, -int(y) + center.Y)
if (decision1 < 0) {
x ++
decisionX += float64(2 * radii.Y * radii.Y)
decision1 += decisionX + float64(radii.Y * radii.Y)
} else {
x ++
y --
decisionX += float64(2 * radii.Y * radii.Y)
decisionY -= float64(2 * radii.X * radii.X)
decision1 +=
decisionX - decisionY +
float64(radii.Y * radii.Y)
}
}
// region 2 decision parameter
decision2 :=
float64(radii.Y * radii.Y) * (x + 0.5) * (x + 0.5) +
float64(radii.X * radii.X) * (y - 1) * (y - 1) -
float64(radii.X * radii.X * radii.Y * radii.Y)
// draw region 2
for y >= 0 {
context.plot( int(x) + center.X, int(y) + center.Y)
context.plot(-int(x) + center.X, int(y) + center.Y)
context.plot( int(x) + center.X, -int(y) + center.Y)
context.plot(-int(x) + center.X, -int(y) + center.Y)
if decision2 > 0 {
y --
decisionY -= float64(2 * radii.X * radii.X)
decision2 += float64(radii.X * radii.X) - decisionY
} else {
y --
x ++
decisionX += float64(2 * radii.Y * radii.Y)
decisionY -= float64(2 * radii.X * radii.X)
decision2 +=
decisionX - decisionY +
float64(radii.X * radii.X)
}
}
}
type ellipsePlottingContext struct {
data []color.RGBA
stride int
source Pattern
width, height int
weight int
bounds image.Rectangle
}
func (context ellipsePlottingContext) plot (x, y int) {
if (image.Point { x, y }).In(context.bounds) {
squareAround (
context.data, context.stride, context.source, x, y,
context.width, context.height, context.weight)
}
}

View File

@ -1,45 +0,0 @@
package artist
import "image/color"
// Gradient is a pattern that interpolates between two colors.
type Gradient struct {
First Pattern
Second Pattern
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Gradient) AtWhen (x, y, width, height int) (c color.RGBA) {
var position float64
switch pattern.Orientation {
case OrientationVertical:
position = float64(y) / float64(height)
case OrientationDiagonalRight:
position = (float64(width - x) / float64(width) +
float64(y) / float64(height)) / 2
case OrientationHorizontal:
position = float64(x) / float64(width)
case OrientationDiagonalLeft:
position = (float64(x) / float64(width) +
float64(y) / float64(height)) / 2
}
firstColor := pattern.First.AtWhen(x, y, width, height)
secondColor := pattern.Second.AtWhen(x, y, width, height)
return LerpRGBA(firstColor, secondColor, position)
}
// Lerp linearally interpolates between two integer values.
func Lerp (first, second int, fac float64) (n int) {
return int(float64(first) * (1 - fac) + float64(second) * fac)
}
// LerpRGBA linearally interpolates between two color.RGBA values.
func LerpRGBA (first, second color.RGBA, fac float64) (c color.RGBA) {
return color.RGBA {
R: uint8(Lerp(int(first.R), int(second.R), fac)),
G: uint8(Lerp(int(first.G), int(second.G), fac)),
B: uint8(Lerp(int(first.G), int(second.B), fac)),
}
}

View File

@ -1,143 +0,0 @@
package artist
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// TODO: draw thick lines more efficiently
// Line draws a line from one point to another with the specified weight and
// pattern.
func Line (
destination tomo.Canvas,
source Pattern,
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 ++
width := updatedRegion.Dx()
height := updatedRegion.Dy()
if abs(max.Y - min.Y) <
abs(max.X - min.X) {
if max.X < min.X {
temp := min
min = max
max = temp
}
lineLow(destination, source, weight, min, max, width, height)
} else {
if max.Y < min.Y {
temp := min
min = max
max = temp
}
lineHigh(destination, source, weight, min, max, width, height)
}
return
}
func lineLow (
destination tomo.Canvas,
source Pattern,
weight int,
min image.Point,
max image.Point,
width, height int,
) {
data, stride := destination.Buffer()
bounds := destination.Bounds()
deltaX := max.X - min.X
deltaY := max.Y - min.Y
yi := 1
if deltaY < 0 {
yi = -1
deltaY *= -1
}
D := (2 * deltaY) - deltaX
y := min.Y
for x := min.X; x < max.X; x ++ {
if !(image.Point { x, y }).In(bounds) { break }
squareAround(data, stride, source, x, y, width, height, weight)
// data[x + y * stride] = source.AtWhen(x, y, width, height)
if D > 0 {
y += yi
D += 2 * (deltaY - deltaX)
} else {
D += 2 * deltaY
}
}
}
func lineHigh (
destination tomo.Canvas,
source Pattern,
weight int,
min image.Point,
max image.Point,
width, height int,
) {
data, stride := destination.Buffer()
bounds := destination.Bounds()
deltaX := max.X - min.X
deltaY := max.Y - min.Y
xi := 1
if deltaX < 0 {
xi = -1
deltaX *= -1
}
D := (2 * deltaX) - deltaY
x := min.X
for y := min.Y; y < max.Y; y ++ {
if !(image.Point { x, y }).In(bounds) { break }
squareAround(data, stride, source, x, y, width, height, weight)
// data[x + y * stride] = source.AtWhen(x, y, width, height)
if D > 0 {
x += xi
D += 2 * (deltaX - deltaY)
} else {
D += 2 * deltaX
}
}
}
func abs (in int) (out int) {
if in < 0 { in *= -1}
out = in
return
}
// TODO: this method of doing things sucks and can cause a segfault. we should
// not be doing it this way
func squareAround (
data []color.RGBA,
stride int,
source Pattern,
x, y, patternWidth, patternHeight, diameter int,
) {
minY := y - diameter + 1
minX := x - diameter + 1
maxY := y + diameter
maxX := x + diameter
for y = minY; y < maxY; y ++ {
for x = minX; x < maxX; x ++ {
data[x + y * stride] =
source.AtWhen(x, y, patternWidth, patternHeight)
}}
}

View File

@ -1,33 +0,0 @@
package artist
import "image/color"
// Noisy is a pattern that randomly interpolates between two patterns in a
// deterministic fashion.
type Noisy struct {
Low Pattern
High Pattern
Seed uint32
Harsh bool
}
// AtWhen satisfies the pattern interface.
func (pattern Noisy) AtWhen (x, y, width, height int) (c color.RGBA) {
// FIXME: this will occasionally generate "clumps"
special := uint32(x + y * 348905)
special += (pattern.Seed + 1) * 15485863
random := (special * special * special % 2038074743)
fac := float64(random) / 2038074743.0
if pattern.Harsh {
if fac > 0.5 {
return pattern.High.AtWhen(x, y, width, height)
} else {
return pattern.Low.AtWhen(x, y, width, height)
}
} else {
return LerpRGBA (
pattern.Low.AtWhen(x, y, width, height),
pattern.High.AtWhen(x, y, width, height), fac)
}
}

View File

@ -1,12 +0,0 @@
package artist
import "image/color"
// Pattern is capable of generating a pattern pixel by pixel.
type Pattern interface {
// AtWhen returns the color of the pixel located at (x, y) relative to
// the origin point of the pattern (0, 0), when the pattern has the
// specified width and height. Patterns may ignore the width and height
// parameters, but it may be useful for some patterns such as gradients.
AtWhen (x, y, width, height int) (color.RGBA)
}

View File

@ -1,95 +0,0 @@
package artist
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
// Paste transfers one canvas onto another, offset by the specified point.
func Paste (
destination tomo.Canvas,
source tomo.Canvas,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
dstData, dstStride := destination.Buffer()
srcData, srcStride := source.Buffer()
sourceBounds :=
source.Bounds().Canon().
Intersect(destination.Bounds().Sub(offset))
if sourceBounds.Empty() { return }
updatedRegion = sourceBounds.Add(offset)
for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
dstData[x + offset.X + (y + offset.Y) * dstStride] =
srcData[x + y * srcStride]
}}
return
}
// FillRectangle draws a filled rectangle with the specified pattern.
func FillRectangle (
destination tomo.Canvas,
source Pattern,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
data, stride := destination.Buffer()
realBounds := bounds
bounds = bounds.Canon().Intersect(destination.Bounds()).Canon()
if bounds.Empty() { return }
updatedRegion = bounds
realWidth, realHeight := realBounds.Dx(), realBounds.Dy()
patternOffset := realBounds.Min.Sub(bounds.Min)
width, height := bounds.Dx(), bounds.Dy()
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
data[x + bounds.Min.X + (y + bounds.Min.Y) * stride] =
source.AtWhen (
x - patternOffset.X, y - patternOffset.Y,
realWidth, realHeight)
}}
return
}
// StrokeRectangle draws the outline of a rectangle with the specified line
// weight and pattern.
func StrokeRectangle (
destination tomo.Canvas,
source Pattern,
weight int,
bounds image.Rectangle,
) {
bounds = bounds.Canon()
insetBounds := bounds.Inset(weight)
if insetBounds.Empty() {
FillRectangle(destination, source, bounds)
return
}
// top
FillRectangle (destination, source, image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Max.X, insetBounds.Min.Y))
// bottom
FillRectangle (destination, source, image.Rect (
bounds.Min.X, insetBounds.Max.Y,
bounds.Max.X, bounds.Max.Y))
// left
FillRectangle (destination, source, image.Rect (
bounds.Min.X, insetBounds.Min.Y,
insetBounds.Min.X, insetBounds.Max.Y))
// right
FillRectangle (destination, source, image.Rect (
insetBounds.Max.X, insetBounds.Min.Y,
bounds.Max.X, insetBounds.Max.Y))
}

View File

@ -1,43 +0,0 @@
package artist
import "image/color"
// Orientation specifies an eight-way pattern orientation.
type Orientation int
const (
OrientationVertical Orientation = iota
OrientationDiagonalRight
OrientationHorizontal
OrientationDiagonalLeft
)
// Split is a pattern that is divided in half between two sub-patterns.
type Split struct {
First Pattern
Second Pattern
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Split) AtWhen (x, y, width, height int) (c color.RGBA) {
var first bool
switch pattern.Orientation {
case OrientationVertical:
first = x < width / 2
case OrientationDiagonalRight:
first = float64(x) / float64(width) +
float64(y) / float64(height) < 1
case OrientationHorizontal:
first = y < height / 2
case OrientationDiagonalLeft:
first = float64(width - x) / float64(width) +
float64(y) / float64(height) < 1
}
if first {
return pattern.First.AtWhen(x, y, width, height)
} else {
return pattern.Second.AtWhen(x, y, width, height)
}
}

View File

@ -1,37 +0,0 @@
package artist
import "image/color"
// Striped is a pattern that produces stripes of two alternating colors.
type Striped struct {
First Stroke
Second Stroke
Orientation
}
// AtWhen satisfies the Pattern interface.
func (pattern Striped) AtWhen (x, y, width, height int) (c color.RGBA) {
position := 0
switch pattern.Orientation {
case OrientationVertical:
position = x
case OrientationDiagonalRight:
position = x + y
case OrientationHorizontal:
position = y
case OrientationDiagonalLeft:
position = x - y
}
phase := pattern.First.Weight + pattern.Second.Weight
position %= phase
if position < 0 {
position += phase
}
if position < pattern.First.Weight {
return pattern.First.AtWhen(x, y, width, height)
} else {
return pattern.Second.AtWhen(x, y, width, height)
}
}

View File

@ -1,356 +0,0 @@
package artist
// import "fmt"
import "image"
import "unicode"
import "image/draw"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo"
type characterLayout struct {
x int
character rune
}
type wordLayout struct {
position image.Point
width int
spaceAfter int
breaksAfter int
text []characterLayout
whitespace []characterLayout
}
// Align specifies a text alignment method.
type Align int
const (
// AlignLeft aligns the start of each line to the beginning point
// of each dot.
AlignLeft Align = iota
AlignRight
AlignCenter
AlignJustify
)
// TextDrawer is a struct that is capable of efficient rendering of wrapped
// text, and calculating text bounds. It avoids doing redundant work
// automatically.
type TextDrawer struct {
runes []rune
face font.Face
width int
height int
align Align
wrap bool
cut bool
layout []wordLayout
layoutClean bool
layoutBounds image.Rectangle
}
// SetText sets the text of the text drawer.
func (drawer *TextDrawer) SetText (runes []rune) {
// if drawer.runes == runes { return }
drawer.runes = runes
drawer.layoutClean = false
}
// SetFace sets the font face of the text drawer.
func (drawer *TextDrawer) SetFace (face font.Face) {
if drawer.face == face { return }
drawer.face = face
drawer.layoutClean = false
}
// SetMaxWidth sets a maximum width for the text drawer, and recalculates the
// layout if needed. If zero is given, there will be no width limit and the text
// will not wrap.
func (drawer *TextDrawer) SetMaxWidth (width int) {
if drawer.width == width { return }
drawer.width = width
drawer.wrap = width != 0
drawer.layoutClean = false
}
// SetMaxHeight sets a maximum height for the text drawer. Lines that are
// entirely below this height will not be drawn, and lines that are on the cusp
// of this maximum height will be clipped at the point that they cross it.
func (drawer *TextDrawer) SetMaxHeight (height int) {
if drawer.height == height { return }
drawer.height = height
drawer.cut = height != 0
drawer.layoutClean = false
}
// SetAlignment specifies how the drawer should align its text. For this to have
// an effect, a maximum width must have been set.
func (drawer *TextDrawer) SetAlignment (align Align) {
if drawer.align == align { return }
drawer.align = align
drawer.layoutClean = false
}
// Draw draws the drawer's text onto the specified canvas at the given offset.
func (drawer *TextDrawer) Draw (
destination tomo.Canvas,
source Pattern,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
wrappedSource := WrappedPattern {
Pattern: source,
Width: 0,
Height: 0, // TODO: choose a better width and height
}
if !drawer.layoutClean { drawer.recalculate() }
// TODO: reimplement a version of draw mask that takes in a pattern and
// only draws to a tomo.Canvas.
for _, word := range drawer.layout {
for _, character := range word.text {
destinationRectangle,
mask, maskPoint, _, ok := drawer.face.Glyph (
fixed.P (
offset.X + word.position.X + character.x,
offset.Y + word.position.Y),
character.character)
if !ok { continue }
// FIXME: clip destination rectangle if we are on the cusp of
// the maximum height.
draw.DrawMask (
destination,
destinationRectangle,
wrappedSource, image.Point { },
mask, maskPoint,
draw.Over)
updatedRegion = updatedRegion.Union(destinationRectangle)
}}
return
}
// LayoutBounds returns a semantic bounding box for text to be used to determine
// an offset for drawing. If a maximum width or height has been set, those will
// be used as the width and height of the bounds respectively. The origin point
// (0, 0) of the returned bounds will be equivalent to the baseline at the start
// of the first line. As such, the minimum of the bounds will be negative.
func (drawer *TextDrawer) LayoutBounds () (bounds image.Rectangle) {
if !drawer.layoutClean { drawer.recalculate() }
bounds = drawer.layoutBounds
return
}
// Em returns the width of an emspace.
func (drawer *TextDrawer) Em () (width fixed.Int26_6) {
if drawer.face == nil { return }
width, _ = drawer.face.GlyphAdvance('M')
return
}
// LineHeight returns the height of one line.
func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
if drawer.face == nil { return }
metrics := drawer.face.Metrics()
height = metrics.Height
return
}
// ReccomendedHeightFor returns the reccomended max height if the text were to
// have its maximum width set to the given width. This does not alter the
// drawer's state.
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
if !drawer.layoutClean { drawer.recalculate() }
metrics := drawer.face.Metrics()
dot := fixed.Point26_6 { 0, metrics.Height }
for _, word := range drawer.layout {
if word.width + dot.X.Round() > width {
dot.Y += metrics.Height
dot.X = 0
}
dot.X += fixed.I(word.width + word.spaceAfter)
if word.breaksAfter > 0 {
dot.Y += fixed.I(word.breaksAfter).Mul(metrics.Height)
dot.X = 0
}
}
return dot.Y.Round()
}
// PositionOf returns the position of the character at the specified index
// relative to the baseline.
func (drawer *TextDrawer) PositionOf (index int) (position image.Point) {
if !drawer.layoutClean { drawer.recalculate() }
index ++
for _, word := range drawer.layout {
position = word.position
for _, character := range word.text {
index --
position.X = word.position.X + character.x
if index < 1 { return }
}
for _, character := range word.whitespace {
index --
position.X = word.position.X + character.x
if index < 1 { return }
}
}
return
}
// Length returns the amount of runes in the drawer's text.
func (drawer *TextDrawer) Length () (length int) {
return len(drawer.runes)
}
func (drawer *TextDrawer) recalculate () {
drawer.layoutClean = true
drawer.layout = nil
drawer.layoutBounds = image.Rectangle { }
if drawer.runes == nil { return }
if drawer.face == nil { return }
metrics := drawer.face.Metrics()
dot := fixed.Point26_6 { 0, 0 }
index := 0
horizontalExtent := 0
currentCharacterX := fixed.Int26_6(0)
previousCharacter := rune(-1)
for index < len(drawer.runes) {
word := wordLayout { }
word.position.X = dot.X.Round()
word.position.Y = dot.Y.Round()
// process a word
currentCharacterX = 0
wordWidth := fixed.Int26_6(0)
for index < len(drawer.runes) && !unicode.IsSpace(drawer.runes[index]) {
character := drawer.runes[index]
_, advance, ok := drawer.face.GlyphBounds(character)
index ++
if !ok { continue }
word.text = append(word.text, characterLayout {
x: currentCharacterX.Round(),
character: character,
})
dot.X += advance
wordWidth += advance
currentCharacterX += advance
if dot.X.Round () > horizontalExtent {
horizontalExtent = dot.X.Round()
}
if previousCharacter >= 0 {
dot.X += drawer.face.Kern (
previousCharacter,
character)
}
previousCharacter = character
}
word.width = wordWidth.Round()
// detect if the word that was just processed goes out of
// bounds, and if it does, wrap it
if drawer.wrap &&
word.width + word.position.X > drawer.width &&
word.position.X > 0 {
word.position.Y += metrics.Height.Round()
word.position.X = 0
dot.Y += metrics.Height
dot.X = wordWidth
}
// process whitespace, going onto a new line if there is a
// newline character
spaceWidth := fixed.Int26_6(0)
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
character := drawer.runes[index]
_, advance, ok := drawer.face.GlyphBounds(character)
index ++
if !ok { continue }
word.whitespace = append(word.whitespace, characterLayout {
x: currentCharacterX.Round(),
character: character,
})
spaceWidth += advance
currentCharacterX += advance
if character == '\n' {
dot.Y += metrics.Height
dot.X = 0
word.breaksAfter ++
break
} else {
dot.X += advance
if previousCharacter >= 0 {
dot.X += drawer.face.Kern (
previousCharacter,
character)
}
}
previousCharacter = character
}
word.spaceAfter = spaceWidth.Round()
// add the word to the layout
drawer.layout = append(drawer.layout, word)
// if there is a set maximum height, and we have crossed it,
// stop processing more words. and remove any words that have
// also crossed the line.
if
drawer.cut &&
(dot.Y - metrics.Ascent - metrics.Descent).Round() >
drawer.height {
for
index := len(drawer.layout) - 1;
index >= 0; index -- {
if drawer.layout[index].position.Y < dot.Y.Round() {
break
}
drawer.layout = drawer.layout[:index]
}
break
}
}
// add a little null to the last character
if len(drawer.layout) > 0 {
lastWord := &drawer.layout[len(drawer.layout) - 1]
lastWord.whitespace = append (
lastWord.whitespace,
characterLayout {
x: currentCharacterX.Round(),
})
}
if drawer.wrap {
drawer.layoutBounds.Max.X = drawer.width
} else {
drawer.layoutBounds.Max.X = horizontalExtent
}
if drawer.cut {
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
drawer.layoutBounds.Max.Y = drawer.height - metrics.Ascent.Round()
} else {
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
drawer.layoutBounds.Max.Y = dot.Y.Round() + metrics.Descent.Round()
}
// TODO:
// for each line, calculate the bounds as if the words are left aligned,
// and then at the end of the process go through each line and re-align
// everything. this will make the process far simpler.
}

View File

@ -1,43 +0,0 @@
package artist
import "image"
import "image/color"
// Texture is a struct that allows an image to be converted into a tiling
// texture pattern.
type Texture struct {
data []color.RGBA
width, height int
}
// NewTexture converts an image into a texture.
func NewTexture (source image.Image) (texture Texture) {
bounds := source.Bounds()
texture.width = bounds.Dx()
texture.height = bounds.Dy()
texture.data = make([]color.RGBA, texture.width * texture.height)
index := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
r, g, b, a := source.At(x, y).RGBA()
texture.data[index] = color.RGBA {
uint8(r >> 8),
uint8(g >> 8),
uint8(b >> 8),
uint8(a >> 8),
}
index ++
}}
return
}
// AtWhen returns the color at the specified x and y coordinates, wrapped to the
// image's width. the width and height are ignored.
func (texture Texture) AtWhen (x, y, width, height int) (pixel color.RGBA) {
x %= texture.width
y %= texture.height
if x < 0 { x += texture.width }
if y < 0 { y += texture.height }
return texture.data[x + y * texture.width]
}

View File

@ -1,55 +0,0 @@
package artist
import "image"
import "image/color"
// Uniform is an infinite-sized pattern of uniform color. It implements the
// Pattern, color.Color, color.Model, and image.Image interfaces.
type Uniform color.RGBA
// NewUniform returns a new Uniform image of the given color.
func NewUniform (c color.Color) (uniform Uniform) {
r, g, b, a := c.RGBA()
uniform.R = uint8(r >> 8)
uniform.G = uint8(g >> 8)
uniform.B = uint8(b >> 8)
uniform.A = uint8(a >> 8)
return
}
// ColorModel satisfies the image.Image interface.
func (uniform Uniform) ColorModel () (model color.Model) {
return uniform
}
// Convert satisfies the color.Model interface.
func (uniform Uniform) Convert (in color.Color) (c color.Color) {
return color.RGBA(uniform)
}
// Bounds satisfies the image.Image interface.
func (uniform Uniform) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 }
return
}
// At satisfies the image.Image interface.
func (uniform Uniform) At (x, y int) (c color.Color) {
return color.RGBA(uniform)
}
// AtWhen satisfies the Pattern interface.
func (uniform Uniform) AtWhen (x, y, width, height int) (c color.RGBA) {
return color.RGBA(uniform)
}
// RGBA satisfies the color.Color interface.
func (uniform Uniform) RGBA () (r, g, b, a uint32) {
return color.RGBA(uniform).RGBA()
}
// Opaque scans the entire image and reports whether it is fully opaque.
func (uniform Uniform) Opaque () (opaque bool) {
return uniform.A == 0xFF
}

View File

@ -1,27 +0,0 @@
package artist
import "image"
import "image/color"
// WrappedPattern is a pattern that is able to behave like an image.Image.
type WrappedPattern struct {
Pattern
Width, Height int
}
// At satisfies the image.Image interface.
func (pattern WrappedPattern) At (x, y int) (c color.Color) {
return pattern.Pattern.AtWhen(x, y, pattern.Width, pattern.Height)
}
// Bounds satisfies the image.Image interface.
func (pattern WrappedPattern) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 }
return
}
// ColorModel satisfies the image.Image interface.
func (pattern WrappedPattern) ColorModel () (model color.Model) {
return color.RGBAModel
}

BIN
assets/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,13 +1,13 @@
package tomo
import "errors"
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 ()
@ -16,45 +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 Window, err error)
// NewEntity creates a new entity for the specified element.
NewEntity (owner Element) Entity
// Copy puts data into the clipboard.
Copy (Data)
// Paste returns the data currently in the clipboard.
Paste (accept []Mime) (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)
// SetConfig sets the configuration of all open windows.
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,338 +0,0 @@
package x
import "git.tebibyte.media/sashakoshka/tomo"
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 tomo.Modifiers,
) {
return tomo.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 == tomo.KeyTab && modifiers.Alt {
if child, ok := window.child.(tomo.Focusable); ok {
direction := tomo.KeynavDirectionForward
if modifiers.Shift {
direction = tomo.KeynavDirectionBackward
}
if !child.HandleFocus(direction) {
child.HandleUnfocus()
}
}
} else if child, ok := window.child.(tomo.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.KeyReleaseEvent)
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.(tomo.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.(tomo.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),
tomo.Button(buttonEvent.Detail))
}
}
}
func (window *Window) handleButtonRelease (
connection *xgbutil.XUtil,
event xevent.ButtonReleaseEvent,
) {
if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok {
buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
child.HandleMouseUp (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
tomo.Button(buttonEvent.Detail))
}
}
func (window *Window) handleMotionNotify (
connection *xgbutil.XUtil,
event xevent.MotionNotifyEvent,
) {
if window.child == nil { return }
if child, ok := window.child.(tomo.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,314 +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"
type Window struct {
backend *Backend
xWindow *xwindow.Window
xCanvas *xgraphics.Image
canvas tomo.BasicCanvas
child tomo.Element
onClose func ()
skipChildDrawCallback bool
metrics struct {
width int
height int
}
}
func (backend *Backend) NewWindow (
width, height int,
) (
output tomo.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.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 tomo.Element) {
// disown previous child
if window.child != nil {
window.child.OnDamage(nil)
window.child.OnMinimumSizeChange(nil)
}
if previousChild, ok := window.child.(tomo.Flexible); ok {
previousChild.OnFlexibleHeightChange(nil)
}
if previousChild, ok := window.child.(tomo.Focusable); ok {
previousChild.OnFocusRequest(nil)
previousChild.OnFocusMotionRequest(nil)
if previousChild.Focused() {
previousChild.HandleUnfocus()
}
}
// adopt new child
window.child = child
if newChild, ok := child.(tomo.Flexible); ok {
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
}
if newChild, ok := child.(tomo.Focusable); ok {
newChild.OnFocusRequest(window.childSelectionRequestCallback)
}
if child != nil {
child.OnDamage(window.childDrawCallback)
child.OnMinimumSizeChange (func () {
window.childMinimumSizeChangeCallback (
child.MinimumSize())
})
window.resizeChildToFit()
window.childMinimumSizeChangeCallback(child.MinimumSize())
window.redrawChildEntirely()
}
}
func (window *Window) Child () (child tomo.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 () {
delete(window.backend.windows, window.xWindow.Id)
if window.onClose != nil { window.onClose() }
xevent.Detach(window.xWindow.X, window.xWindow.Id)
window.xWindow.Destroy()
}
func (window *Window) OnClose (callback func ()) {
window.onClose = callback
}
func (window *Window) reallocateCanvas () {
window.canvas = tomo.NewBasicCanvas (
window.metrics.width,
window.metrics.height)
if window.xCanvas != nil {
window.xCanvas.Destroy()
}
window.xCanvas = xgraphics.New (
window.backend.connection,
image.Rect (
0, 0,
window.metrics.width,
window.metrics.height))
window.xCanvas.CreatePixmap()
}
func (window *Window) redrawChildEntirely () {
window.pushRegion(window.paste(window.child))
}
func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true
if child, ok := window.child.(tomo.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 tomo.Canvas) {
if window.skipChildDrawCallback { return }
window.pushRegion(window.paste(region))
}
func (window *Window) paste (canvas tomo.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) {
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)
}
}
func (window *Window) childSelectionRequestCallback () (granted bool) {
if child, ok := window.child.(tomo.Focusable); ok {
child.HandleFocus(tomo.KeynavDirectionNeutral)
}
return true
}
func (window *Window) childSelectionMotionRequestCallback (
direction tomo.KeynavDirection,
) (
granted bool,
) {
if child, ok := window.child.(tomo.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,103 +0,0 @@
package x
import "git.tebibyte.media/sashakoshka/tomo"
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
}
windows map[xproto.Window] *Window
}
// NewBackend instantiates an X backend.
func NewBackend () (output tomo.Backend, err error) {
backend := &Backend {
windows: map[xproto.Window] *Window { },
doChannel: make(chan func (), 0),
}
// 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()
for _, window := range backend.windows {
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 tomo.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 []tomo.Mime) (data tomo.Data) {
backend.assert()
// TODO
return
}
func (backend *Backend) assert () {
if backend == nil { panic("nil backend") }
}
func init () {
tomo.RegisterBackend(NewBackend)
}

View File

@ -1,72 +0,0 @@
package tomo
import "image"
import "image/draw"
import "image/color"
// 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
}
// 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
}
// 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
}

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
}

20
data.go
View File

@ -1,20 +0,0 @@
package tomo
import "io"
// Data represents arbitrary polymorphic data that can be used for data transfer
// between applications.
type Data map[Mime] io.ReadCloser
// Mime represents a MIME type.
type Mime struct {
// Type is the first half of the MIME type, and Subtype is the second
// half. The separating slash is not included in either. For example,
// text/html becomes:
// Mime { Type: "text", Subtype: "html" }
Type, Subtype string
}
var MimePlain = Mime { "text", "plain" }
var MimeFile = Mime { "text", "uri-list" }

57
data/data.go Normal file
View File

@ -0,0 +1,57 @@
// 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.ReadSeekCloser
// Mime represents a MIME type.
type Mime struct {
// Type is the first half of the MIME type, and Subtype is the second
// half. The separating slash is not included in either. For example,
// text/html becomes:
// Mime { Type: "text", Subtype: "html" }
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 },
},
}

83
dirs/dirs.go Normal file
View File

@ -0,0 +1,83 @@
// Package dirs provides access to standard system and user directories.
package dirs
import "os"
import "strings"
import "path/filepath"
var homeDirectory string
var configHome string
var configDirs []string
var dataHome string
var dataDirs []string
var cacheHome string
func init () {
var err error
homeDirectory, err = os.UserHomeDir()
if err != nil {
panic("could not get user home directory: " + err.Error())
}
configHome = os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
configHome = filepath.Join(homeDirectory, "/.config/")
}
configDirsString := os.Getenv("XDG_CONFIG_DIRS")
if configDirsString == "" {
configDirsString = "/etc/xdg/"
}
configDirs = append(strings.Split(configDirsString, ":"), configHome)
dataHome = os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
dataHome = filepath.Join(homeDirectory, "/.local/share/")
}
dataDirsString := os.Getenv("XDG_CONFIG_DIRS")
if dataDirsString == "" {
dataDirsString = "/usr/local/share/:/usr/share/"
}
configDirs = append(strings.Split(configDirsString, ":"), configHome)
cacheHome = os.Getenv("XDG_CACHE_HOME")
if cacheHome == "" {
cacheHome = filepath.Join(homeDirectory, "/.cache/")
}
}
// ConfigHome returns the path to the directory where user configuration files
// should be stored.
func ConfigHome (name string) (home string) {
return filepath.Join(configHome, name)
}
// ConfigDirs returns all paths where configuration files might exist.
func ConfigDirs (name string) (dirs []string) {
dirs = make([]string, len(configDirs))
for index, dir := range configDirs {
dirs[index] = filepath.Join(dir, name)
}
return
}
// DataHome returns the path to the directory where user data should be stored.
func DataHome (name string) (home string) {
return filepath.Join(dataHome, name)
}
// DataDirs returns all paths where data files might exist.
func DataDirs (name string) (dirs []string) {
dirs = make([]string, len(dataDirs))
for index, dir := range dataDirs {
dirs[index] = filepath.Join(dir, name)
}
return
}
// CacheHome returns the path to the directory where user cache files should be
// stored.
func CacheHome (name string) (home string) {
return filepath.Join(cacheHome, name)
}

View File

@ -1,177 +1,15 @@
package tomo
import "image"
import "art"
// Element represents a basic on-screen object.
// Element represents a basic on-screen object. Extended element interfaces are
// defined in the ability module.
type Element interface {
// Element must implement the Canvas interface. Elements should start
// out with a completely blank buffer, and only allocate memory and draw
// on it for the first time when sent an EventResize event.
Canvas
// 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)
// 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)
// 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)
// 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))
// OnMinimumSizeChange sets a function to be called when the element's
// minimum size is changed.
OnMinimumSizeChange (callback func ())
}
// KeynavDirection represents a keyboard navigation direction.
type KeynavDirection int
const (
KeynavDirectionNeutral KeynavDirection = 0
KeynavDirectionBackward KeynavDirection = -1
KeynavDirectionForward KeynavDirection = 1
)
// Canon returns a well-formed direction.
func (direction KeynavDirection) Canon () (canon KeynavDirection) {
if direction > 0 {
return KeynavDirectionForward
} else if direction == 0 {
return KeynavDirectionNeutral
} else {
return KeynavDirectionBackward
}
}
// 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 is 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 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.
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 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 Key, modifiers Modifiers)
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
HandleKeyUp (key Key, modifiers 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 Button)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element.
HandleMouseUp (x, y int, button 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 ())
// Entity returns this element's entity.
Entity () Entity
}

View File

@ -1,153 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var buttonCase = theme.C("basic", "button")
// Button is a clickable button.
type Button struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
drawer artist.TextDrawer
pressed bool
text string
onClick func ()
}
// NewButton creates a new button with the specified label text.
func NewButton (text string) (element *Button) {
element = &Button { }
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.drawer.SetFace(theme.FontFaceRegular())
element.SetText(text)
return
}
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if button != tomo.ButtonLeft { return }
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Button) HandleMouseUp (x, y int, button tomo.Button) {
if button != tomo.ButtonLeft { return }
element.pressed = false
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
within := image.Point { x, y }.
In(element.Bounds())
if !element.Enabled() { return }
if within && element.onClick != nil {
element.onClick()
}
}
func (element *Button) HandleMouseMove (x, y int) { }
func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Button) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
if !element.Enabled() { return }
if key == tomo.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
func (element *Button) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.KeyEnter && element.pressed {
element.pressed = false
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
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))
textBounds := element.drawer.LayoutBounds()
_, inset := theme.ButtonPattern(theme.PatternState { Case: buttonCase })
minimumSize := inset.Inverse().Apply(textBounds).Inset(-theme.Padding())
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Button) draw () {
bounds := element.Bounds()
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: buttonCase,
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element, pattern, bounds)
innerBounds := inset.Apply(bounds)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
X: innerBounds.Min.X + (innerBounds.Dx() - textBounds.Dx()) / 2,
Y: innerBounds.Min.Y + (innerBounds.Dy() - textBounds.Dy()) / 2,
}
// account for the fact that the bounding rectangle will be shifted over
// due to the bounds origin being at the baseline of the first line
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Case: buttonCase,
Disabled: !element.Enabled(),
})
element.drawer.Draw(element, foreground, offset)
}

View File

@ -1,170 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var checkboxCase = theme.C("basic", "checkbox")
// Checkbox is a toggle-able checkbox with a label.
type Checkbox struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
drawer artist.TextDrawer
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 }
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.drawer.SetFace(theme.FontFaceRegular())
element.SetText(text)
return
}
func (element *Checkbox) HandleMouseDown (x, y int, button tomo.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 tomo.Button) {
if button != tomo.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 tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
func (element *Checkbox) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.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))
textBounds := element.drawer.LayoutBounds()
if text == "" {
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
} else {
element.core.SetMinimumSize (
textBounds.Dy() + theme.Padding() + textBounds.Dx(),
textBounds.Dy())
}
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)
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: checkboxCase,
})
artist.FillRectangle(element, backgroundPattern, bounds)
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: checkboxCase,
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element, pattern, boxBounds)
textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + theme.Padding(),
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Case: checkboxCase,
Disabled: !element.Enabled(),
})
element.drawer.Draw(element, foreground, offset)
if element.checked {
checkBounds := inset.Apply(boxBounds).Inset(2)
artist.FillRectangle(element, foreground, checkBounds)
}
}

View File

@ -1,475 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var containerCase = theme.C("basic", "container")
// Container is an element capable of containg other elements, and arranging
// them in a layout.
type Container struct {
*core.Core
core core.CoreControl
layout tomo.Layout
children []tomo.LayoutEntry
drags [10]tomo.MouseTarget
warping bool
focused bool
focusable bool
flexible bool
onFocusRequest func () (granted bool)
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool)
onFlexibleHeightChange func ()
}
// NewContainer creates a new container.
func NewContainer (layout tomo.Layout) (element *Container) {
element = &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 tomo.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 tomo.Element, expand bool) {
// set event handlers
child.OnDamage (func (region tomo.Canvas) {
element.core.DamageRegion(region.Bounds())
})
child.OnMinimumSizeChange(element.updateMinimumSize)
if child0, ok := child.(tomo.Flexible); ok {
child0.OnFlexibleHeightChange(element.updateMinimumSize)
}
if child0, ok := child.(tomo.Focusable); ok {
child0.OnFocusRequest (func () (granted bool) {
return element.childFocusRequestCallback(child0)
})
child0.OnFocusMotionRequest (
func (direction tomo.KeynavDirection) (granted bool) {
if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction)
})
}
// add child
element.children = append (element.children, tomo.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 tomo.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 tomo.Element) {
child.DrawTo(nil)
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
if child0, ok := child.(tomo.Focusable); ok {
child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil)
if child0.Focused() {
child0.HandleUnfocus()
}
}
if child0, ok := child.(tomo.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 []tomo.Element) {
children = make([]tomo.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 tomo.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 tomo.Element) {
for _, entry := range element.children {
if point.In(entry.Bounds) {
child = entry.Element
}
}
return
}
func (element *Container) childPosition (child tomo.Element) (position image.Point) {
for _, entry := range element.children {
if entry.Element == child {
position = entry.Bounds.Min
break
}
}
return
}
func (element *Container) redoAll () {
// do a layout
element.recalculate()
// draw a background
bounds := element.Bounds()
pattern, _ := theme.BackgroundPattern (theme.PatternState {
Case: containerCase,
})
artist.FillRectangle(element, pattern, bounds)
// cut our canvas up and give peices to child elements
for _, entry := range element.children {
entry.DrawTo(tomo.Cut(element, entry.Bounds))
}
}
func (element *Container) HandleMouseDown (x, y int, button tomo.Button) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
if !handlesMouse { return }
element.drags[button] = child
child.HandleMouseDown(x, y, button)
}
func (element *Container) HandleMouseUp (x, y int, button tomo.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)).(tomo.MouseTarget)
if !handlesMouse { return }
child.HandleMouseScroll(x, y, deltaX, deltaY)
}
func (element *Container) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
element.forFocused (func (child tomo.Focusable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyDown(key, modifiers)
}
return true
})
}
func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
element.forFocused (func (child tomo.Focusable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyUp(key, modifiers)
}
return true
})
}
func (element *Container) FlexibleHeightFor (width int) (height int) {
return element.layout.FlexibleHeightFor(element.children, 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 tomo.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 tomo.KeynavDirectionNeutral, tomo.KeynavDirectionForward:
// if we recieve a neutral or forward direction, focus
// the first focusable element.
return element.focusFirstFocusableElement(direction)
case tomo.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.(tomo.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.(tomo.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 tomo.KeynavDirection,
) (
ok bool,
) {
element.forFocusable (func (child tomo.Focusable) bool {
if child.HandleFocus(direction) {
element.focused = true
ok = true
return false
}
return true
})
return
}
func (element *Container) focusLastFocusableElement (
direction tomo.KeynavDirection,
) (
ok bool,
) {
element.forFocusableBackward (func (child tomo.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 tomo.Focusable) bool {
child.HandleUnfocus()
return true
})
}
func (element *Container) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback
}
func (element *Container) OnFocusMotionRequest (
callback func (direction tomo.KeynavDirection) (granted bool),
) {
element.onFocusMotionRequest = callback
}
func (element *Container) forFocused (callback func (child tomo.Focusable) bool) {
for _, entry := range element.children {
child, focusable := entry.Element.(tomo.Focusable)
if focusable && child.Focused() {
if !callback(child) { break }
}
}
}
func (element *Container) forFocusable (callback func (child tomo.Focusable) bool) {
for _, entry := range element.children {
child, focusable := entry.Element.(tomo.Focusable)
if focusable {
if !callback(child) { break }
}
}
}
func (element *Container) forFlexible (callback func (child tomo.Flexible) bool) {
for _, entry := range element.children {
child, flexible := entry.Element.(tomo.Flexible)
if flexible {
if !callback(child) { break }
}
}
}
func (element *Container) forFocusableBackward (callback func (child tomo.Focusable) bool) {
for index := len(element.children) - 1; index >= 0; index -- {
child, focusable := element.children[index].Element.(tomo.Focusable)
if focusable {
if !callback(child) { break }
}
}
}
func (element *Container) firstFocused () (index int) {
for currentIndex, entry := range element.children {
child, focusable := entry.Element.(tomo.Focusable)
if focusable && child.Focused() {
return currentIndex
}
}
return -1
}
func (element *Container) reflectChildProperties () {
element.focusable = false
element.forFocusable (func (tomo.Focusable) bool {
element.focusable = true
return false
})
element.flexible = false
element.forFlexible (func (tomo.Flexible) bool {
element.flexible = true
return false
})
if !element.focusable {
element.focused = false
}
}
func (element *Container) childFocusRequestCallback (
child tomo.Focusable,
) (
granted bool,
) {
if element.onFocusRequest != nil && element.onFocusRequest() {
element.forFocused (func (child tomo.Focusable) bool {
child.HandleUnfocus()
return true
})
child.HandleFocus(tomo.KeynavDirectionNeutral)
return true
} else {
return false
}
}
func (element *Container) updateMinimumSize () {
width, height := element.layout.MinimumSize(element.children)
if element.flexible {
height = element.layout.FlexibleHeightFor(element.children, width)
}
element.core.SetMinimumSize(width, height)
}
func (element *Container) recalculate () {
element.layout.Arrange(element.children, element.Bounds())
}

View File

@ -1,122 +0,0 @@
package basic
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var labelCase = theme.C("basic", "label")
// Label is a simple text box.
type Label struct {
*core.Core
core core.CoreControl
wrap bool
text string
drawer artist.TextDrawer
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.Core, element.core = core.NewCore(element.handleResize)
face := theme.FontFaceRegular()
element.drawer.SetFace(face)
element.SetWrap(wrap)
element.SetText(text)
return
}
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()
}
}
func (element *Label) updateMinimumSize () {
if element.wrap {
em := element.drawer.Em().Round()
if em < 1 { em = theme.Padding() }
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, _ := theme.BackgroundPattern(theme.PatternState {
Case: labelCase,
})
artist.FillRectangle(element, pattern, bounds)
textBounds := element.drawer.LayoutBounds()
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Case: labelCase,
})
element.drawer.Draw (element, foreground, bounds.Min.Sub(textBounds.Min))
}

View File

@ -1,396 +0,0 @@
package basic
import "fmt"
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var listCase = theme.C("basic", "list")
// 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
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.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)
}
element.draw()
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) {
element.forcedMinimumWidth = width
element.forcedMinimumHeight = height
element.updateMinimumSize()
}
func (element *List) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if button != tomo.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 tomo.Button) {
if button != tomo.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 tomo.Key, modifiers tomo.Modifiers) {
if !element.Enabled() { return }
altered := false
switch key {
case tomo.KeyLeft, tomo.KeyUp:
altered = element.changeSelectionBy(-1)
case tomo.KeyRight, tomo.KeyDown:
altered = element.changeSelectionBy(1)
case tomo.KeyEscape:
altered = element.selectEntry(-1)
}
if altered && element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *List) HandleKeyUp(key tomo.Key, modifiers tomo.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) {
_, inset := theme.ListPattern(theme.PatternState {
Case: listCase,
})
return element.Bounds().Dy() - inset[0] - inset[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.Collapse(element.forcedMinimumWidth)
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.Collapse(element.forcedMinimumWidth)
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.Collapse(element.forcedMinimumWidth)
element.entries[index] = entry
// redraw
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *List) selectUnderMouse (x, y int) (updated bool) {
_, inset := theme.ListPattern(theme.PatternState { })
bounds := inset.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) {
_, inset := theme.ListPattern(theme.PatternState {
Case: listCase,
})
entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1])
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.Bounds().Dx()
if entryWidth > minimumWidth {
minimumWidth = entryWidth
}
}
}
if minimumHeight == 0 {
minimumHeight = element.contentHeight
}
_, inset := theme.ListPattern(theme.PatternState {
Case: listCase,
})
minimumHeight += inset[0] + inset[2]
element.core.SetMinimumSize(minimumWidth, minimumHeight)
}
func (element *List) draw () {
bounds := element.Bounds()
pattern, inset := theme.ListPattern(theme.PatternState {
Case: listCase,
Disabled: !element.Enabled(),
Focused: element.Focused(),
})
artist.FillRectangle(element, pattern, bounds)
bounds = inset.Apply(bounds)
dot := image.Point {
bounds.Min.X,
bounds.Min.Y - element.scroll,
}
innerCanvas := tomo.Cut(element, bounds)
for index, entry := range element.entries {
entryPosition := dot
dot.Y += entry.Bounds().Dy()
if dot.Y < bounds.Min.Y { continue }
if entryPosition.Y > bounds.Max.Y { break }
entry.Draw (
innerCanvas, entryPosition,
element.Focused(), element.selectedEntry == index)
}
}

View File

@ -1,91 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
var listEntryCase = theme.C("basic", "listEntry")
// ListEntry is an item that can be added to a list.
type ListEntry struct {
drawer artist.TextDrawer
bounds image.Rectangle
textPoint image.Point
text string
forcedMinimumWidth int
onSelect func ()
}
func NewListEntry (text string, onSelect func ()) (entry ListEntry) {
entry = ListEntry {
text: text,
onSelect: onSelect,
}
entry.drawer.SetText([]rune(text))
entry.drawer.SetFace(theme.FontFaceRegular())
entry.updateBounds()
return
}
func (entry *ListEntry) Collapse (width int) {
if entry.forcedMinimumWidth == width { return }
entry.forcedMinimumWidth = width
entry.updateBounds()
}
func (entry *ListEntry) updateBounds () {
entry.bounds = image.Rectangle { }
entry.bounds.Max.Y = entry.drawer.LineHeight().Round()
if entry.forcedMinimumWidth > 0 {
entry.bounds.Max.X = entry.forcedMinimumWidth
} else {
entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx()
}
_, inset := theme.ItemPattern(theme.PatternState {
})
entry.bounds.Max.Y += inset[0] + inset[2]
entry.textPoint =
image.Pt(inset[3], inset[0]).
Sub(entry.drawer.LayoutBounds().Min)
}
func (entry *ListEntry) Draw (
destination tomo.Canvas,
offset image.Point,
focused bool,
on bool,
) (
updatedRegion image.Rectangle,
) {
pattern, _ := theme.ItemPattern(theme.PatternState {
Case: listEntryCase,
Focused: focused,
On: on,
})
artist.FillRectangle (
destination,
pattern,
entry.Bounds().Add(offset))
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Case: listEntryCase,
Focused: focused,
On: on,
})
return entry.drawer.Draw (
destination,
foreground,
offset.Add(entry.textPoint))
}
func (entry *ListEntry) RunSelect () {
if entry.onSelect != nil {
entry.onSelect()
}
}
func (entry *ListEntry) Bounds () (bounds image.Rectangle) {
return entry.bounds
}

View File

@ -1,46 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo/theme"
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
}
// NewProgressBar creates a new progress bar displaying the given progress
// level.
func NewProgressBar (progress float64) (element *ProgressBar) {
element = &ProgressBar { progress: progress }
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(theme.Padding() * 2, theme.Padding() * 2)
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()
}
}
func (element *ProgressBar) draw () {
bounds := element.Bounds()
pattern, inset := theme.SunkenPattern(theme.PatternState { })
artist.FillRectangle(element, pattern, bounds)
bounds = inset.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
accent, _ := theme.AccentPattern(theme.PatternState { })
artist.FillRectangle(element, accent, meterBounds)
}

View File

@ -1,473 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var scrollContainerCase = theme.C("basic", "scrollContainer")
var scrollBarHorizontalCase = theme.C("basic", "scrollBarHorizontal")
var scrollBarVerticalCase = theme.C("basic", "scrollBarVertical")
// ScrollContainer is a container that is capable of holding a scrollable
// element.
type ScrollContainer struct {
*core.Core
core core.CoreControl
focused bool
child tomo.Scrollable
childWidth, childHeight int
horizontal struct {
exists bool
enabled bool
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
vertical struct {
exists bool
enabled bool
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
onFocusRequest func () (granted bool)
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool)
}
// NewScrollContainer creates a new scroll container with the specified scroll
// bars.
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
element = &ScrollContainer { }
element.Core, element.core = core.NewCore(element.handleResize)
element.updateMinimumSize()
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 tomo.Scrollable) {
// disown previous child if it exists
if element.child != nil {
element.clearChildEventHandlers(child)
}
// adopt new child
element.child = child
if child != nil {
child.OnDamage(element.childDamageCallback)
child.OnMinimumSizeChange(element.updateMinimumSize)
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
if newChild, ok := child.(tomo.Focusable); ok {
newChild.OnFocusRequest (
element.childFocusRequestCallback)
newChild.OnFocusMotionRequest (
element.childFocusMotionRequestCallback)
}
// TODO: somehow inform the core that we do not in fact want to
// redraw the element.
element.updateMinimumSize()
element.horizontal.enabled,
element.vertical.enabled = element.child.ScrollAxes()
if element.core.HasImage() {
element.resizeChildToFit()
}
}
}
func (element *ScrollContainer) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
if child, ok := element.child.(tomo.KeyboardTarget); ok {
child.HandleKeyDown(key, modifiers)
}
}
func (element *ScrollContainer) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
if child, ok := element.child.(tomo.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers)
}
}
func (element *ScrollContainer) HandleMouseDown (x, y int, button tomo.Button) {
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) {
// FIXME: x backend and scroll container should pull these
// values from the same place
if x > element.horizontal.bar.Min.X {
element.scrollChildBy(16, 0)
} else {
element.scrollChildBy(-16, 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) {
if y > element.vertical.bar.Min.Y {
element.scrollChildBy(0, 16)
} else {
element.scrollChildBy(0, -16)
}
} else if child, ok := element.child.(tomo.MouseTarget); ok {
child.HandleMouseDown(x, y, button)
}
}
func (element *ScrollContainer) HandleMouseUp (x, y int, button tomo.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.(tomo.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.(tomo.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 {
element.onFocusRequest()
}
}
func (element *ScrollContainer) HandleFocus (
direction tomo.KeynavDirection,
) (
accepted bool,
) {
if child, ok := element.child.(tomo.Focusable); ok {
element.focused = true
return child.HandleFocus(direction)
} else {
element.focused = false
return false
}
}
func (element *ScrollContainer) HandleUnfocus () {
if child, ok := element.child.(tomo.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 tomo.KeynavDirection) (granted bool),
) {
element.onFocusMotionRequest = callback
}
func (element *ScrollContainer) childDamageCallback (region tomo.Canvas) {
element.core.DamageRegion(artist.Paste(element, region, image.Point { }))
}
func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
child, ok := element.child.(tomo.Focusable)
if !ok { return false }
if element.onFocusRequest != nil && element.onFocusRequest() {
child.HandleFocus(tomo.KeynavDirectionNeutral)
return true
} else {
return false
}
}
func (element *ScrollContainer) childFocusMotionRequestCallback (
direction tomo.KeynavDirection,
) (
granted bool,
) {
if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction)
}
func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable) {
child.DrawTo(nil)
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
child.OnScrollBoundsChange(nil)
if child0, ok := child.(tomo.Focusable); ok {
child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil)
if child0.Focused() {
child0.HandleUnfocus()
}
}
if child0, ok := child.(tomo.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(tomo.Cut(element, childBounds))
}
func (element *ScrollContainer) recalculate () {
_, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
})
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
})
horizontal := &element.horizontal
vertical := &element.vertical
bounds := element.Bounds()
thicknessHorizontal :=
theme.HandleWidth() +
gutterInsetHorizontal[3] +
gutterInsetHorizontal[1]
thicknessVertical :=
theme.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 () {
artist.Paste(element, element.child, image.Point { })
deadPattern, _ := theme.DeadPattern(theme.PatternState {
Case: scrollContainerCase,
})
artist.FillRectangle (
element, 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 () {
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
Case: scrollBarHorizontalCase,
Disabled: !element.horizontal.enabled,
})
artist.FillRectangle(element, gutterPattern, element.horizontal.gutter)
handlePattern, _ := theme.HandlePattern (theme.PatternState {
Case: scrollBarHorizontalCase,
Disabled: !element.horizontal.enabled,
Pressed: element.horizontal.dragging,
})
artist.FillRectangle(element, handlePattern, element.horizontal.bar)
}
func (element *ScrollContainer) drawVerticalBar () {
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
Case: scrollBarVerticalCase,
Disabled: !element.vertical.enabled,
})
artist.FillRectangle(element, gutterPattern, element.vertical.gutter)
handlePattern, _ := theme.HandlePattern (theme.PatternState {
Case: scrollBarVerticalCase,
Disabled: !element.vertical.enabled,
Pressed: element.vertical.dragging,
})
artist.FillRectangle(element, 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 := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
})
_, gutterInsetVertical := theme.GutterPattern(theme.PatternState {
Case: scrollBarHorizontalCase,
})
thicknessHorizontal :=
theme.HandleWidth() +
gutterInsetHorizontal[3] +
gutterInsetHorizontal[1]
thicknessVertical :=
theme.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,52 +0,0 @@
package basic
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var spacerCase = theme.C("basic", "spacer")
// Spacer can be used to put space between two elements..
type Spacer struct {
*core.Core
core core.CoreControl
line bool
}
// 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.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(1, 1)
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
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Spacer) draw () {
bounds := element.Bounds()
if element.line {
pattern, _ := theme.ForegroundPattern(theme.PatternState {
Case: spacerCase,
Disabled: true,
})
artist.FillRectangle(element, pattern, bounds)
} else {
pattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: spacerCase,
Disabled: true,
})
artist.FillRectangle(element, pattern, bounds)
}
}

View File

@ -1,194 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var switchCase = theme.C("basic", "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 {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
drawer artist.TextDrawer
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 }
element.Core, element.core = core.NewCore(element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.drawer.SetFace(theme.FontFaceRegular())
element.drawer.SetText([]rune(text))
element.calculateMinimumSize()
return
}
func (element *Switch) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Switch) HandleMouseUp (x, y int, button tomo.Button) {
if button != tomo.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 tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
func (element *Switch) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.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 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.calculateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Switch) calculateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
lineHeight := element.drawer.LineHeight().Round()
if element.text == "" {
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
} else {
element.core.SetMinimumSize (
lineHeight * 2 + theme.Padding() + 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)
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState {
Case: switchCase,
})
artist.FillRectangle (element, backgroundPattern, 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, _ := theme.GutterPattern(theme.PatternState {
Case: switchCase,
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element, gutterPattern, gutterBounds)
handlePattern, _ := theme.HandlePattern(theme.PatternState {
Case: switchCase,
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element, handlePattern, handleBounds)
textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() * 2 + theme.Padding(),
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Case: switchCase,
Disabled: !element.Enabled(),
})
element.drawer.Draw(element, foreground, offset)
}

View File

@ -1,334 +0,0 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
var textBoxCase = theme.C("basic", "textBox")
// TextBox is a single-line text input.
type TextBox struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
cursor int
scroll int
placeholder string
text []rune
placeholderDrawer artist.TextDrawer
valueDrawer artist.TextDrawer
onKeyDown func (key tomo.Key, modifiers tomo.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.Core, element.core = core.NewCore(element.handleResize)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.placeholderDrawer.SetFace(theme.FontFaceRegular())
element.valueDrawer.SetFace(theme.FontFaceRegular())
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 tomo.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
}
func (element *TextBox) HandleMouseUp (x, y int, button tomo.Button) { }
func (element *TextBox) HandleMouseMove (x, y int) { }
func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *TextBox) HandleKeyDown(key tomo.Key, modifiers tomo.Modifiers) {
if element.onKeyDown != nil && element.onKeyDown(key, modifiers) {
return
}
scrollMemory := element.scroll
altered := true
textChanged := false
switch {
case key == tomo.KeyBackspace:
if len(element.text) < 1 { break }
element.text, element.cursor = textmanip.Backspace (
element.text,
element.cursor,
modifiers.Control)
textChanged = true
case key == tomo.KeyDelete:
if len(element.text) < 1 { break }
element.text, element.cursor = textmanip.Delete (
element.text,
element.cursor,
modifiers.Control)
textChanged = true
case key == tomo.KeyLeft:
element.cursor = textmanip.MoveLeft (
element.text,
element.cursor,
modifiers.Control)
case key == tomo.KeyRight:
element.cursor = textmanip.MoveRight (
element.text,
element.cursor,
modifiers.Control)
case key.Printable():
element.text, element.cursor = textmanip.Type (
element.text,
element.cursor,
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.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *TextBox) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { }
func (element *TextBox) SetPlaceholder (placeholder string) {
if element.placeholder == placeholder { return }
element.placeholder = placeholder
element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *TextBox) SetValue (text string) {
// if element.text == text { return }
element.text = []rune(text)
element.runOnChange()
element.valueDrawer.SetText(element.text)
if element.cursor > element.valueDrawer.Length() {
element.cursor = element.valueDrawer.Length()
}
element.scrollToCursor()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
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 tomo.Key, modifiers tomo.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) {
return element.Bounds().Inset(theme.Padding()).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 }
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
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) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds()
_, inset := theme.InputPattern(theme.PatternState {
Case: textBoxCase,
})
element.core.SetMinimumSize (
textBounds.Dx() +
theme.Padding() * 2 + inset[3] + inset[1],
element.placeholderDrawer.LineHeight().Round() +
theme.Padding() * 2 + inset[0] + inset[2])
}
func (element *TextBox) runOnChange () {
if element.onChange != nil {
element.onChange()
}
}
func (element *TextBox) scrollToCursor () {
if !element.core.HasImage() { return }
bounds := element.Bounds().Inset(theme.Padding())
bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round()
cursorPosition := element.valueDrawer.PositionOf(element.cursor)
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 }
}
}
func (element *TextBox) draw () {
bounds := element.Bounds()
// FIXME: take index into account
pattern, inset := theme.InputPattern(theme.PatternState {
Case: textBoxCase,
Disabled: !element.Enabled(),
Focused: element.Focused(),
})
artist.FillRectangle(element, pattern, bounds)
if len(element.text) == 0 && !element.Focused() {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
offset := bounds.Min.Add (image.Point {
X: theme.Padding() + inset[3],
Y: theme.Padding() + inset[0],
})
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: textBoxCase,
Disabled: true,
})
element.placeholderDrawer.Draw (
element,
foreground,
offset.Sub(textBounds.Min))
} else {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
offset := bounds.Min.Add (image.Point {
X: theme.Padding() + inset[3] - element.scroll,
Y: theme.Padding() + inset[0],
})
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: textBoxCase,
Disabled: !element.Enabled(),
})
element.valueDrawer.Draw (
element,
foreground,
offset.Sub(textBounds.Min))
if element.Focused() {
// cursor
cursorPosition := element.valueDrawer.PositionOf (
element.cursor)
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: textBoxCase,
})
artist.Line (
element,
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,149 +0,0 @@
package core
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// 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 tomo.Canvas
metrics struct {
minimumWidth int
minimumHeight int
}
drawSizeChange func ()
onMinimumSizeChange func ()
onDamage func (region tomo.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
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) ColorModel () (model color.Model) {
return color.RGBAModel
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) At (x, y int) (pixel color.Color) {
if core.canvas == nil { return }
return core.canvas.At(x, y)
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) Bounds () (bounds image.Rectangle) {
if core.canvas == nil { return }
return core.canvas.Bounds()
}
// ColorModel fulfills the draw.Image interface.
func (core *Core) Set (x, y int, c color.Color) () {
if core.canvas == nil { return }
core.canvas.Set(x, y, c)
}
// Buffer fulfills the tomo.Canvas interface.
func (core *Core) Buffer () (data []color.RGBA, stride int) {
if core.canvas == nil { return }
return core.canvas.Buffer()
}
// 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 tomo.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 tomo.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
}
// 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 (bounds image.Rectangle) {
if control.core.onDamage != nil {
control.core.onDamage(tomo.Cut(control.core, bounds))
}
}
// 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,111 +0,0 @@
package core
import "git.tebibyte.media/sashakoshka/tomo"
// 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(tomo.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 { return }
if core.onFocusRequest != nil {
core.onFocusRequest()
}
}
// 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 tomo.KeynavDirection,
) (
accepted bool,
) {
direction = direction.Canon()
if !core.enabled { return false }
if core.focused && direction != tomo.KeynavDirectionNeutral {
return 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 tomo.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())
}

3
elements/doc.go Normal file
View File

@ -0,0 +1,3 @@
// 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())
}

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

@ -3,56 +3,49 @@ package fun
import "time"
import "math"
import "image"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "image/color"
import "tomo"
import "art"
import "art/shapes"
var clockCase = theme.C("fun", "clock")
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
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.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
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
// Entity returns this element's entity.
func (element *AnalogClock) Entity () tomo.Entity {
return element.entity
}
func (element *AnalogClock) draw () {
bounds := element.Bounds()
// Draw causes the element to draw to the specified destination canvas.
func (element *AnalogClock) Draw (destination art.Canvas) {
bounds := element.entity.Bounds()
pattern, inset := theme.SunkenPattern(theme.PatternState {
Case: clockCase,
})
artist.FillRectangle(element, pattern, 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 = inset.Apply(bounds)
bounds = padding.Apply(bounds)
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Case: clockCase,
})
accent, _ := theme.AccentPattern(theme.PatternState {
Case: clockCase,
})
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)
}
@ -61,35 +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 calle dwhen 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 (
source artist.Pattern,
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)))
// println(min.String(), max.String())
artist.Line(element, source, 1, min, max)
shapes.ColorLine(destination, source, 1, min, max)
}

3
elements/fun/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package fun provides "fun" elements that have few actual use cases, but serve
// as good demos of what Tomo is capable of.
package fun

View File

@ -0,0 +1,4 @@
// Package music provides types relating to music theory and the math behind it.
// It is used in the fun.Piano element, and in the piano example to generate
// pitches from notes.
package music

View File

@ -0,0 +1,70 @@
package music
import "math"
var semitone = math.Pow(2, 1.0 / 12.0)
// Tuning is an interface representing a tuning.
type Tuning interface {
// Tune returns the frequency of a given note in Hz.
Tune (Note) float64
}
// EqualTemparment implements twelve-tone equal temparment.
type EqualTemparment struct { A4 float64 }
// Tune returns the EqualTemparment frequency of a given note in Hz.
func (tuning EqualTemparment) Tune (note Note) float64 {
return tuning.A4 * math.Pow(semitone, float64(note - NoteA4))
}
// Octave represents a MIDI octave.
type Octave int
// Note returns the note at the specified scale degree in the chromatic scale.
func (octave Octave) Note (degree int) Note {
return Note(int(octave + 1) * 12 + degree)
}
// Note represents a MIDI note.
type Note int
const (
NoteC0 Note = iota
NoteDb0
NoteD0
NoteEb0
NoteE0
NoteF0
NoteGb0
NoteG0
NoteAb0
NoteA0
NoteBb0
NoteB0
// nice
NoteA4 Note = 69
)
// Octave returns the octave of the note
func (note Note) Octave () Octave {
return Octave(note / 12 - 1)
}
// Degree returns the scale degree of the note in the chromatic scale.
func (note Note) Degree () int {
mod := note % 12
if mod < 0 { mod += 12 }
return int(mod)
}
// IsSharp returns whether or not the note is a sharp.
func (note Note) IsSharp () bool {
degree := note.Degree()
return degree == 1 ||
degree == 3 ||
degree == 6 ||
degree == 8 ||
degree == 10
}

326
elements/fun/piano.go Normal file
View File

@ -0,0 +1,326 @@
package fun
import "image"
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
type pianoKey struct {
image.Rectangle
music.Note
}
// Piano is an element that can be used to input midi notes.
type Piano struct {
entity tomo.Entity
low, high music.Octave
flatKeys []pianoKey
sharpKeys []pianoKey
contentBounds image.Rectangle
enabled bool
pressed *pianoKey
keynavPressed map[music.Note] bool
onPress func (music.Note)
onRelease func (music.Note)
}
// 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 { low, high = high, low }
element = &Piano {
low: low,
high: high,
keynavPressed: make(map[music.Note] bool),
}
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
}
// OnRelease sets a function to be called when a key is released.
func (element *Piano) OnRelease (callback func (note music.Note)) {
element.onRelease = callback
}
func (element *Piano) HandleMouseDown (x, y int, button input.Button) {
element.Focus()
if button != input.ButtonLeft { return }
element.pressUnderMouseCursor(image.Pt(x, y))
}
func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
if element.onRelease != nil && element.pressed != nil {
element.onRelease((*element.pressed).Note)
}
element.pressed = nil
element.entity.Invalidate()
}
func (element *Piano) HandleMotion (x, y int) {
if element.pressed == nil { return }
element.pressUnderMouseCursor(image.Pt(x, y))
}
func (element *Piano) pressUnderMouseCursor (point image.Point) {
// find out which note is being pressed
newKey := (*pianoKey)(nil)
for index, key := range element.flatKeys {
if point.In(key.Rectangle) {
newKey = &element.flatKeys[index]
break
}
}
for index, key := range element.sharpKeys {
if point.In(key.Rectangle) {
newKey = &element.sharpKeys[index]
break
}
}
if newKey == nil { return }
if newKey != element.pressed {
// release previous note
if element.pressed != nil && element.onRelease != nil {
element.onRelease((*element.pressed).Note)
}
// press new note
element.pressed = newKey
if element.onPress != nil {
element.onPress((*element.pressed).Note)
}
element.entity.Invalidate()
}
}
var noteForKey = map[input.Key] music.Note {
'a': 46,
'z': 47,
'x': 48,
'd': 49,
'c': 50,
'f': 51,
'v': 52,
'b': 53,
'h': 54,
'n': 55,
'j': 56,
'm': 57,
'k': 58,
',': 59,
'.': 60,
';': 61,
'/': 62,
'\'': 63,
'1': 56,
'q': 57,
'2': 58,
'w': 59,
'e': 60,
'4': 61,
'r': 62,
'5': 63,
't': 64,
'y': 65,
'7': 66,
'u': 67,
'8': 68,
'i': 69,
'9': 70,
'o': 71,
'p': 72,
'-': 73,
'[': 74,
'=': 75,
']': 76,
'\\': 77,
}
func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
note, exists := noteForKey[key]
if !exists { return }
if !element.keynavPressed[note] {
element.keynavPressed[note] = true
if element.onPress != nil {
element.onPress(note)
}
element.entity.Invalidate()
}
}
func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
note, exists := noteForKey[key]
if !exists { return }
_, pressed := element.keynavPressed[note]
if !pressed { return }
delete(element.keynavPressed, note)
if element.onRelease != nil {
element.onRelease(note)
}
element.entity.Invalidate()
}
func (element *Piano) HandleThemeChange () {
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Piano) updateMinimumSize () {
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
element.entity.SetMinimumSize (
pianoKeyWidth * 7 * element.countOctaves() +
padding.Horizontal(),
64 + padding.Vertical())
}
func (element *Piano) countOctaves () int {
return int(element.high - element.low + 1)
}
func (element *Piano) countFlats () int {
return element.countOctaves() * 8
}
func (element *Piano) countSharps () int {
return element.countOctaves() * 5
}
func (element *Piano) recalculate () {
element.flatKeys = make([]pianoKey, element.countFlats())
element.sharpKeys = make([]pianoKey, element.countSharps())
padding := element.entity.Theme().Padding(tomo.PatternPinboard, pianoCase)
bounds := padding.Apply(element.entity.Bounds())
dot := bounds.Min
note := element.low.Note(0)
limit := element.high.Note(12)
flatIndex := 0
sharpIndex := 0
for note < limit {
if note.IsSharp() {
element.sharpKeys[sharpIndex].Rectangle = image.Rect (
-(pianoKeyWidth * 3) / 7, 0,
(pianoKeyWidth * 3) / 7,
(bounds.Dy() * 5) / 8).Add(dot)
element.sharpKeys[sharpIndex].Note = note
sharpIndex ++
} else {
element.flatKeys[flatIndex].Rectangle = image.Rect (
0, 0, pianoKeyWidth, bounds.Dy()).Add(dot)
dot.X += pianoKeyWidth
element.flatKeys[flatIndex].Note = note
flatIndex ++
}
note ++
}
element.contentBounds = image.Rectangle {
bounds.Min,
image.Pt(dot.X, bounds.Max.Y),
}
}
func (element *Piano) drawFlat (
destination art.Canvas,
bounds image.Rectangle,
pressed bool,
state tomo.State,
) {
state.Pressed = pressed
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 tomo.State,
) {
state.Pressed = pressed
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)
}

58
elements/lerpslider.go Normal file
View File

@ -0,0 +1,58 @@
package elements
import "tomo"
// Numeric is a type constraint representing a number.
type Numeric interface {
~float32 | ~float64 |
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// 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
min T
max T
}
// 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] {
min: min,
max: max,
}
element.entity = tomo.GetBackend().NewEntity(element)
element.construct()
element.SetValue(value)
return
}
// SetValue sets the slider's value.
func (element *LerpSlider[T]) SetValue (value T) {
value -= element.min
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())) +
element.min
}
// Range returns the difference between the slider's maximum and minimum values.
func (element *LerpSlider[T]) Range () T {
return element.max - 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,319 +4,206 @@ import "fmt"
import "time"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
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
cellBounds image.Rectangle
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(480, 600)
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(240, 240)
return
}
func (element *Artist) draw () {
bounds := element.Bounds()
element.cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
element.cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8
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
artist.FillRectangle (
element,
artist.Beveled {
artist.NewUniform(hex(0xFF0000FF)),
artist.NewUniform(hex(0x0000FFFF)),
},
element.cellAt(0, 0))
// 1, 0
artist.StrokeRectangle (
element,
artist.NewUniform(hex(0x00FF00FF)), 3,
element.cellAt(1, 0))
// 2, 0
artist.FillRectangle (
element,
artist.NewMultiBordered (
artist.Stroke { Pattern: uhex(0xFF0000FF), Weight: 1 },
artist.Stroke { Pattern: uhex(0x888800FF), Weight: 2 },
artist.Stroke { Pattern: uhex(0x00FF00FF), Weight: 3 },
artist.Stroke { Pattern: uhex(0x008888FF), Weight: 4 },
artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
),
element.cellAt(2, 0))
// 3, 0
artist.FillRectangle (
element,
artist.Bordered {
Stroke: artist.Stroke { Pattern: uhex(0x0000FFFF), Weight: 5 },
Fill: uhex(0xFF0000FF),
},
element.cellAt(3, 0))
// 0, 0 - 3, 0
for x := 0; x < 4; x ++ {
element.colorLines(destination, x + 1, element.cellAt(destination, x, 0).Bounds())
}
// 4, 0
artist.FillRectangle (
element,
artist.Padded {
Stroke: uhex(0xFFFFFFFF),
Fill: uhex(0x666666FF),
Sides: []int { 4, 13, 2, 0 },
},
element.cellAt(4, 0))
c40 := element.cellAt(destination, 4, 0)
shapes.StrokeColorRectangle(c40, artutil.Hex(0x888888FF), c40.Bounds(), 1)
shapes.ColorLine (
c40, artutil.Hex(0xFF0000FF), 1,
c40.Bounds().Min, c40.Bounds().Max)
// 0, 1 - 3, 1
for x := 0; x < 4; x ++ {
artist.FillRectangle (
element,
artist.Striped {
First: artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 7 },
Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 2 },
Orientation: artist.Orientation(x),
},
element.cellAt(x, 1))
}
// 0, 1
c01 := element.cellAt(destination, 0, 1)
shapes.StrokeColorRectangle(c01, artutil.Hex(0x888888FF), c01.Bounds(), 1)
shapes.FillColorEllipse(destination, artutil.Hex(0x00FF00FF), c01.Bounds())
// 0, 2 - 3, 2
for x := 0; x < 4; x ++ {
element.lines(x + 1, element.cellAt(x, 2))
}
// 0, 3
artist.StrokeRectangle (
element,uhex(0x888888FF), 1,
element.cellAt(0, 3))
artist.FillEllipse(element, uhex(0x00FF00FF), element.cellAt(0, 3))
// 1, 3 - 3, 3
// 1, 1 - 3, 1
for x := 1; x < 4; x ++ {
artist.StrokeRectangle (
element,uhex(0x888888FF), 1,
element.cellAt(x, 3))
artist.StrokeEllipse (
element,
[]artist.Pattern {
uhex(0xFF0000FF),
uhex(0x00FF00FF),
uhex(0xFF00FFFF),
c := element.cellAt(destination, x, 1)
shapes.StrokeColorRectangle (
destination, artutil.Hex(0x888888FF),
c.Bounds(), 1)
shapes.StrokeColorEllipse (
destination,
[]color.RGBA {
artutil.Hex(0xFF0000FF),
artutil.Hex(0x00FF00FF),
artutil.Hex(0xFF00FFFF),
} [x - 1],
x, element.cellAt(x, 3))
c.Bounds(), x)
}
// 0, 4 - 3, 4
for x := 0; x < 4; x ++ {
artist.FillEllipse (
element,
artist.Split {
First: uhex(0xFF0000FF),
Second: uhex(0x0000FFFF),
Orientation: artist.Orientation(x),
},
element.cellAt(x, 4))
// 4, 1
c41 := element.cellAt(destination, 4, 1)
shatterPos := c41.Bounds().Min
rocks := []image.Rectangle {
image.Rect(3, 12, 13, 23).Add(shatterPos),
// image.Rect(30, 10, 40, 23).Add(shatterPos),
image.Rect(55, 40, 70, 49).Add(shatterPos),
image.Rect(30, -10, 40, 43).Add(shatterPos),
image.Rect(80, 30, 90, 45).Add(shatterPos),
}
tiles := shatter.Shatter(c41.Bounds(), rocks...)
for index, tile := range tiles {
[]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(destination, 0, 2)
shapes.StrokeColorRectangle(c02, artutil.Hex(0x888888FF), c02.Bounds(), 1)
shapes.FillEllipse(c02, c41, c02.Bounds())
// 1, 2
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(destination, 2, 2)
shapes.FillRectangle(c22, c41, c22.Bounds())
// 3, 2
c32 := element.cellAt(destination, 3, 2)
shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
// 4, 2
c42 := element.cellAt(destination, 4, 2)
// 0, 3
c03 := element.cellAt(destination, 0, 3)
patterns.Border {
Canvas: element.thingy(c42),
Inset: art.Inset { 8, 8, 8, 8 },
}.Draw(c03, c03.Bounds())
// 1, 3
c13 := element.cellAt(destination, 1, 3)
patterns.Border {
Canvas: element.thingy(c42),
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 := artist.TextDrawer { }
textDrawer.SetFace(defaultfont.FaceRegular)
textDrawer := textdraw.Drawer { }
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, uhex(0xFFFFFFFF), image.Pt(8, bounds.Max.Y - 24))
// 0, 5
artist.FillRectangle (
element,
artist.QuadBeveled {
uhex(0x880000FF),
uhex(0x00FF00FF),
uhex(0x0000FFFF),
uhex(0xFF00FFFF),
},
element.cellAt(0, 5))
// 1, 5
artist.FillRectangle (
element,
artist.Checkered {
First: artist.QuadBeveled {
uhex(0x880000FF),
uhex(0x00FF00FF),
uhex(0x0000FFFF),
uhex(0xFF00FFFF),
},
Second: artist.Striped {
First: artist.Stroke { Pattern: uhex(0xFF8800FF), Weight: 1 },
Second: artist.Stroke { Pattern: uhex(0x0088FFFF), Weight: 1 },
Orientation: artist.OrientationVertical,
},
CellWidth: 32,
CellHeight: 16,
},
element.cellAt(1, 5))
// 2, 5
artist.FillRectangle (
element,
artist.Dotted {
Foreground: uhex(0x00FF00FF),
Background: artist.Checkered {
First: uhex(0x444444FF),
Second: uhex(0x888888FF),
CellWidth: 16,
CellHeight: 16,
},
Size: 8,
Spacing: 16,
},
element.cellAt(2, 5))
// 3, 5
artist.FillRectangle (
element,
artist.Tiled {
Pattern: artist.QuadBeveled {
uhex(0x880000FF),
uhex(0x00FF00FF),
uhex(0x0000FFFF),
uhex(0xFF00FFFF),
},
CellWidth: 17,
CellHeight: 23,
},
element.cellAt(3, 5))
// 0, 6 - 3, 6
for x := 0; x < 4; x ++ {
artist.FillRectangle (
element,
artist.Gradient {
First: uhex(0xFF0000FF),
Second: uhex(0x0000FFFF),
Orientation: artist.Orientation(x),
},
element.cellAt(x, 6))
}
// 0, 7
artist.FillEllipse (
element,
artist.EllipticallyBordered {
Fill: artist.Gradient {
First: uhex(0x00FF00FF),
Second: uhex(0x0000FFFF),
Orientation: artist.OrientationVertical,
},
Stroke: artist.Stroke { Pattern: uhex(0x00FF00), Weight: 5 },
},
element.cellAt(0, 7))
// 1, 7
artist.FillRectangle (
element,
artist.Noisy {
Low: uhex(0x000000FF),
High: uhex(0xFFFFFFFF),
Seed: 0,
},
element.cellAt(1, 7),
)
// 2, 7
artist.FillRectangle (
element,
artist.Noisy {
Low: uhex(0x000000FF),
High: artist.Gradient {
First: uhex(0x000000FF),
Second: uhex(0xFFFFFFFF),
Orientation: artist.OrientationVertical,
},
Seed: 0,
},
element.cellAt(2, 7),
)
// 3, 7
artist.FillRectangle (
element,
artist.Noisy {
Low: uhex(0x000000FF),
High: uhex(0xFFFFFFFF),
Seed: 0,
Harsh: true,
},
element.cellAt(3, 7),
)
textDrawer.Draw (
destination, artutil.Hex(0xFFFFFFFF),
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
}
func (element *Artist) lines (weight int, bounds image.Rectangle) {
func (element *Artist) colorLines (destination art.Canvas, weight int, bounds image.Rectangle) {
bounds = bounds.Inset(4)
c := uhex(0xFFFFFFFF)
artist.Line(element, c, weight, bounds.Min, bounds.Max)
artist.Line (
element, c, weight,
c := artutil.Hex(0xFFFFFFFF)
shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max)
shapes.ColorLine (
destination, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y),
image.Pt(bounds.Min.X, bounds.Max.Y))
artist.Line (
element, c, weight,
shapes.ColorLine (
destination, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
artist.Line (
element, c, weight,
shapes.ColorLine (
destination, c, weight,
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
artist.Line (
element, c, weight,
shapes.ColorLine (
destination, c, weight,
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
artist.Line (
element, c, weight,
shapes.ColorLine (
destination, c, weight,
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
artist.Line (
element, c, weight,
shapes.ColorLine (
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))
artist.Line (
element, c, weight,
shapes.ColorLine (
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) (image.Rectangle) {
return element.cellBounds.Add (image.Pt (
x * element.cellBounds.Dx(),
y * element.cellBounds.Dy()))
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 art.Cut (destination, cellBounds.Add (image.Pt (
x * cellBounds.Dx(),
y * cellBounds.Dy())))
}
func hex (n uint32) (c color.RGBA) {
c.A = uint8(n)
c.B = uint8(n >> 8)
c.G = uint8(n >> 16)
c.R = uint8(n >> 24)
return
}
func uhex (n uint32) (artist.Pattern) {
return artist.NewUniform (color.RGBA {
A: uint8(n),
B: uint8(n >> 8),
G: uint8(n >> 16),
R: uint8(n >> 24),
})
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, 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)
}

3
elements/testing/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package testing provides elements that are used to test different parts of
// Tomo's API.
package testing

View File

@ -1,70 +1,86 @@
package testing
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
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
color artist.Pattern
entity tomo.Entity
pressed bool
lastMousePos image.Point
}
// NewMouse creates a new mouse test element.
func NewMouse () (element *Mouse) {
element = &Mouse { }
element.Core, element.core = core.NewCore(element.draw)
element.core.SetMinimumSize(32, 32)
element.color = artist.NewUniform(color.Black)
element.entity = tomo.GetBackend().NewEntity(element)
element.entity.SetMinimumSize(32, 32)
return
}
func (element *Mouse) draw () {
bounds := element.Bounds()
pattern, _ := theme.AccentPattern(theme.PatternState { })
artist.FillRectangle(element, pattern, bounds)
artist.StrokeRectangle (
element,
artist.NewUniform(color.Black), 1,
bounds)
artist.Line (
element, artist.NewUniform(color.White), 1,
func (element *Mouse) Entity () tomo.Entity {
return element.entity
}
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 (
destination,
artutil.Hex(0x000000FF),
bounds, 1)
shapes.ColorLine (
destination, artutil.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
artist.Line (
element, artist.NewUniform(color.White), 1,
shapes.ColorLine (
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 tomo.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 tomo.Button) {
element.drawing = false
mousePos := image.Pt(x, y)
element.core.DamageRegion (artist.Line (
element, element.color, 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 (artist.Line (
element, element.color, 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,
}
}

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,17 +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"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(128, 128)
window.SetTitle("Draw Test")
window.Adopt(testing.NewArtist())
window.OnClose(tomo.Stop)
window.Show()
}

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 := basic.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"
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 := basic.NewContainer(layouts.Vertical { true, true })
window.Adopt(container)
container.Adopt (basic.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(basic.NewSpacer(true), false)
container.Adopt(basic.NewCheckbox("Oh god", false), false)
container.Adopt(basic.NewCheckbox("Can you hear them", true), false)
container.Adopt(basic.NewCheckbox("They are in the walls", false), false)
container.Adopt(basic.NewCheckbox("They are coming for us", false), false)
disabledCheckbox := basic.NewCheckbox("We are but their helpless prey", false)
disabledCheckbox.SetEnabled(false)
container.Adopt(disabledCheckbox, false)
vsync := basic.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 := basic.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"
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 := basic.NewContainer(layouts.Dialog { true, true })
window.Adopt(container)
container.Adopt(basic.NewLabel("you will explode", true), true)
cancel := basic.NewButton("Cancel")
cancel.SetEnabled(false)
container.Adopt(cancel, false)
okButton := basic.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"
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 := basic.NewContainer(layouts.Vertical { true, true })
window.Adopt(container)
var world flow.Flow
world.Transition = container.DisownAll
world.Stages = map [string] func () {
"start": func () {
label := basic.NewLabel (
"you are standing next to a river.", true)
button0 := basic.NewButton("go in the river")
button0.OnClick(world.SwitchFunc("wet"))
button1 := basic.NewButton("walk along the river")
button1.OnClick(world.SwitchFunc("house"))
button2 := basic.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 := basic.NewLabel (
"you get completely soaked.\n" +
"you die of hypothermia.", true)
button0 := basic.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := basic.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 := basic.NewLabel (
"you are standing in front of a delapidated " +
"house.", true)
button1 := basic.NewButton("go inside")
button1.OnClick(world.SwitchFunc("inside"))
button0 := basic.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 := basic.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 := basic.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 := basic.NewLabel (
"you come face to face with a bear.\n" +
"it eats you (it was hungry).", true)
button0 := basic.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := basic.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"
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)
window.SetTitle("clock")
container := basic.NewContainer(layouts.Vertical { true, true })
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")
window.SetApplicationName("TomoClock")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
clock := fun.NewAnalogClock(time.Now())
container.Adopt(clock, true)
label := basic.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 *basic.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"
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 := basic.NewContainer(layouts.Horizontal { true, true })
window.Adopt(container)
container.Adopt(basic.NewLabel("this is sample text", true), true)
container.Adopt(basic.NewLabel("this is sample text", true), true)
container.Adopt(basic.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
}

63
examples/image/image.go Normal file
View File

@ -0,0 +1,63 @@
package main
import "os"
import "image"
import "bytes"
import _ "image/png"
import "github.com/jezek/xgbutil/gopher"
import "tomo"
import "tomo/nasin"
import "tomo/popups"
import "tomo/elements"
func main () {
nasin.Run(Application { })
}
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 { return err }
logo, _, err := image.Decode(file)
file.Close()
if err != nil { return err }
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.AdoptExpand(logoImage)
container.Adopt(button)
window.Adopt(container)
button.Focus()
window.OnClose(nasin.Stop)
window.Show()
return nil
}
func fatalError (window tomo.Window, err error) {
popups.NewDialog (
popups.DialogKindError,
window,
"Error",
err.Error(),
popups.Button {
Name: "OK",
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"
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 := basic.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
// create inputs
firstName := basic.NewTextBox("First name", "")
lastName := basic.NewTextBox("Last name", "")
fingerLength := basic.NewTextBox("Length of fingers", "")
button := basic.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(basic.NewLabel("Choose your words carefully.", false), true)
container.Adopt(firstName, false)
container.Adopt(lastName, false)
container.Adopt(fingerLength, false)
container.Adopt(basic.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