Last active
July 28, 2023 06:35
-
-
Save laughinghan/bffa95855d25f718d2b1ca0bf61c8e9e to your computer and use it in GitHub Desktop.
Revisions
-
laughinghan revised this gist
Dec 5, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -11,7 +11,7 @@ The code is much simpler than PinLayout's: ```ts const logo = new UIImage('/logo.png', 100, 100) const segmented = new UISegmentedControl(['Intro', '1', '2']) const text = new UIText('Swift manual views layouting without auto layout, no magic, pure code …') const separator = new UISeparator(1, 'blue') return layout.pad_all(10) -
laughinghan revised this gist
Dec 5, 2021 . 1 changed file with 4 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -23,9 +23,11 @@ return layout.pad_all(10) .cut_top(separator) ``` This was inspired by [RectCut](https://halt.software/dead-simple-layouts/), but instead of requiring upfront a fixed pixel size of cut to make, you can have the cut fit to the size of the content. It does this in one pass, without reflow, by "recursing" into the content and asking for its size. This system can also be thought of as a tree of nested columns and rows. Just like PinLayout, this system is Just Code, so you still have full control & flexibility, you can do your own math on what size cut to make or put in if-statements wherever you want. This could even augment PinLayout by simplifying common usages but preserving an identical level of control. (Some stuff that hasn't been implemented but easily could: min/max sizes on items/cuts; a way to add "overhang" to make a layout "stick out" past where it was cut somewhere higher in the tree, necessary for layouts [like this](https://benvitalenum3ers.files.wordpress.com/2014/02/four-rectangles-square.png); right-to-left or upwards layout) - [Visual test with HTML rendering in JSBin](https://jsbin.com/loqapel/edit?js,output) - [TypeScript Playground IDE for the source code](https://www.typescriptlang.org/play?target=1#code/PTAEAsBdIBwZwFwgFZwEYEsB2A6AxgPYC2wANgQI4CGMApqcLQCYaQD8qANAQK6x8AoAQDMeWPJAwEsoAE60sTWrIAUpKgE9ekBKAAym7QEpQAbwGhLoQljiRQ5AOYErVgLygstAO6gAqgCSAURUjrQqAORkBM44MFiOEZygAIwADGnJ6WlGFlY2dqBwtI5ECpDMoB5evoEAyiVlWBVMAMLSkLIEpCoA2hEBzV1JoBEpIxEATBEAurmuBfYVAB72rlWePv4BACq0q5F13hjC9iFYPFSkoABuGD5wDoZ82I6gx5Dg2qBUfC7qWj4ySwLhCjgweGSMB48msBCUyVEpGuNk63VAOExEXm+WkhWKMCosiokAIsg2NW2DUJxNJqhSyQiaFIPFo2KErnkkBhMgB2jiVCYAH0rj10jjLPg+ELSTAVHAeMznvY3AA+PLrIqKvl8KWQIWkWinNQxAjJUw-UgYRxYXQRWURUAAXyMAuFhuN4o16z1MoIcuKpXKzFdhOFspUXs1kuhkBUK0gEtASd9EYJRJJZNy3uAACpvQBBUgVWRYEkYG60UAFgAKAXerC+fGs5AV8keADIG59vnhwFRsK8EByrDrIG6RcjIzkR5ZFlqlYCVU8l6n-SokwrF-y8NKPXGnGazJbrbbRg7nZvtcqJ-vp1ft7rd-q041g0wH2OJxGo1Yt1-Y3jfZE1nFcd2lV8aUzWQk1zYABCdIR5wAMQAeQAOR2IUAHUAgAER2AAJSwPHSAQUIwrDCIAUQCABxQidg2FIADYhBAUBPirVFiQkUBhDJOQFCUVQjF0bwqwqZFQEBTjwCrKgbgHdRmSrY4mE+UAu3k60oFAFRaEbZRQEEtACE+AQOIIYR3nACFwFAEINFANAqwiYRWAAWlRcpsWSKhFBk3hOPoa4eEebx+3sOAMAALyrQEImk1ycFAai3gwMFaAijBkUssA4DwK5JJcDz7GwGKlDkhSlNyqhVNAFgmhivFkkEhMG2kpQSyIbAq1YR4dMcPS0CoYomHykyZC4hsNPAHBJoAaRBbwZAwOAFVoXQxoVMpHnkRRlA3ITuVLR4Yvix5DQ2uSAtAcFKxkRTlPqw0iji7L-MC448o4vBiRgEzmxOUBWFuKR1AqR5PhJARsBLYQqDwKtAmow0mnscxOWEo7nrq1ShXUz5dAuIhXPJAAfUYyu8jpfP82qVMNIUhqgEmeDJ4yqfcryfOabFdAAJVoQVpFIDQAB4LSJ8B2c52RklZnRPA58nnXVRDJsy0JsqKIq3tJfiwYqjAqpmvGmarJqFBa2woXbZQ7gSaqMHJeHZBNvBJrGug+NpKQAG5QZsszNIt161NNzTtMM4b7CJNyab5yARi46b5NBoZPcmi7+secLmFSosSzLSRK10JzXKKJtSCYKbxb14rONK42bPDhrrdsKRbG92E4BCZFjJh9P+qzru8He+K2FAOoXEKq5Xn4pG6U664rm8TRHirwATIhSNhyPUG7AmCHXQaIGB0fKR5Ucv5ozG9RZZB4CQyRUeRRawBuYVIXQ7A9hJkjvyYGLZyMs5bk0ASLYBn9nJK3AcoEwphELY0OqoC2hMo6yxVvLUA3Mk5035gzZSLNY5s2wWrPBvMCEp0Qd6VwHEyo-C3taJhoN7BFRkJFXKVYYAO1kE7N4PtaB+3LAQOho5aD2BlorUhkBxGWBBiodBMsqhuA8DzSAtNmi+S0l2dBStVHqPwdo-mtDozrBUNLTBMjdL2CdBsT460kyuHsfQYowd9LKMwYY6mVCTEp3keY3BVNHFwBwErYAoScAqNVJ4xmJDbGsNJuTIwwAvFzTMUEywBiPD6NkUk1WyhAmuBUR4KJMtcwRKiUrQJrjSDuKxlk2amlcnxJlsUqwOS5LrXCbI3MMtIl2TCe08xyDoxch5MeaREB8ljMsIhTWDCMDLEqErZIKj9iEkUNDFu7COgDi8LIJaK01obVZOJKKHj25vRUetBw2Vob9hkL1ThmD954EPtfAIDQgzaLaB0Lo1xMoX1oBjL5aNQXlHvgsPEnRn50jflAkBbDQWICEh-Buf9Xi9DmGYOZQlUFKLaZg+BlNfGaOTiMPJtjSW4PJVoiopjoXRkUeklpaj6WUsyVkiZpYpkku6WE1gqKcDIAINgSIVhsQ4ENAkT4uY0KYRwvhIiNi466EVVRWiDEmL4pcaANxVZGk8skZMqxc1trErmmqshmqhQ0XooxZ0tTvQLPYmAUa40po-EZhHZp4BkjdWUC83WM0DFeGYIXSaAQbIqLEI-A5zBkgzUit0SSwFQaPFjN66QVYrReAWhxOoNc65g37JWH4jUjS-GLP6zOdgoH7w4stAgq1M2bV0MA7KWB7SNRcO-a4HVvAAxgK8ZIFUKiCgNXYbWUMXJjUqNIUAAAiSAGhR3617iLZdJk27WH7LxEsB8drbD2KsM+IKwXbAhRjZlc5YVPxfqoIByKEy-06K8RB+KDoiSJcQsB5CuacuoVS+JcDANko0Qy3y3L1gcWwPcjAPBd3osHRm4dNBR0ANALwyRwlbpLAzXcqgoBJh4QcBgCgPBTbdgcnAYgCkiC8DvtZFdZHbjdA5rQZdaT36BPnAnEjZShk4ATDKhQjh5V2twgRQiCrKL2u1YxQJhopHWJmbYwJrKrXsqMX4xlKddE+uITkjlUGuV3qCV0u1DqdUdMsKUwVongLibleAeTSqZNEVqQa+p-UbJ-tyhguaPjzMgdg+Yrp1K44FPlvZutuT37ACVhTNIPnDWWfMY5tl4B4tRaSzLVLLrzG8pkOaz4Nq7GuoQu6iAZI4r7OuOmWkgl820EmgAIQUFqRwYQG111CdXXgtcXJVi3LtSoaBnIkY3s5Q2TBiS+DukSTAnQiTOXkBIAKjhDTHqPt82gUEV7AtvpAcFp3LOP3ha-F9MCNPqog5AjFzlCDkFkO+-+jgv3eh-bjHTWDklAbC-40DJnZG0soRS8LmWrClf5Ra4zQXSlmeMQZx009JhpFAJa-96nwPVNmdVzWHyT0GCXJZjiMbqqZ1YBgK4iPLZ1sEgY4jRsVl10BwraqI8wKCHoWAH6a9SCzcGopSSlyVHM-yQFAbGdrmRxCzL+7elBc5gKuAf+ABrVhM0xyZusM2HggNDbZscQkR42AFr89AIRVttBKxuxsjNQckh6fy6Z+SFnjxl2o-KDulQZQAqL1YBEOAavQDIHCvYdQsgwgGpBDwYazcjblTO3CfxRhk3yR53rwXloRejZT9B5onBw9K4nVA6qYAxyh7rUrgxefHBdCW4814jwE44eBjIKb4fnySGdmDI3VurAcXQuZcXJJBX6+wEoOgh074CXJCo+vsjkj0YI7XkE9hJigE1zPsP1vWNjjgFnkkteAAsu-9-6Q8uzyetBETLMqNM2-qzV+gHjQ+xNH5h+WA4jsDOWUTyQ0Ssa4QkOPO5EEWQAeBuV-OuZfQKT-WwNbPqOuJWX-dPP+a7VQQJDiTuW2R4VjeQEIQcZ2d3OAQkZGTOKfMcYccxDiAsR4JQQqD2VyOueqAgSscdPdOA+-d4MaGnV3ZEZyXhTg02JNbnaqXAsAQgWQdsGAaQFgMg31DuTKG2buVeauLXHXDOPXO5Z8D-GAaQ5PU3Oyc3TOIONAYGGyMGDeC3LAWnUUZyYHNHLPbrLiYw93fAjQvPdeTeQvFw3yDA1waEZkCEBnCOYLYmSwTnOlQIwhQJUIq0CeeXBJB7WIyHYvFOUvYwoA-cOEMkJQkkXWIg0FA5Recgyg0eGg5UOg6MBgx5PNZUfXAwo3Ew5sM3RwewthNw9xPPJjCtYBNtGXXIlwGaD2OOYIqwXhCsYoiIgmZYDYTIRIj2JSCoeY5mZyDwZY4wxGTXKsZdX7T7HdVjQIXzSFZoLeZyZdBbKgY4BIHdLiV2ISLbBIa6cdHAWgVKTDYSV4UYmg9aJYDOQgQo7AYo9vQKXOdvKROyPsZPW43wEWOEs4+gC4uRErJFO7TbM7XQAsOQzQKWZYUlZIDQYk-1Mk8DTnZIZOXQG+NE9WDYHFb0CLKwcpbxDYVIl) -
laughinghan created this gist
Dec 5, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,31 @@ # Minimalist Layout System This super simple, fast, flexible layout system operates in a single pass (no reflow or constraint solving) and is probably <100 lines of non-comment, non-duplicate logic (there's a lot of duplicate logic between the top/left/right/bottom directional code that's not worth de-duplicating). To layout PinLayout's Example 1: [](https://github.com/layoutBox/PinLayout#introduction-examples) The code is much simpler than PinLayout's: ```ts const logo = new UIImage('/logo.png', 100, 100) const segmented = new UISegmentedControl(['Intro', '1', '2']) const text = new UIText('Swift manual views layouting without auto layout, no magic, pure code, full control ...') const separator = new UISeparator(1, 'blue') return layout.pad_all(10) .cut_top(sublayout => sublayout.cut_left(logo, { align: 'top' }).pad_left(10) .cut_top(segmented).pad_top(10) .put(text) ) .cut_top(separator) ``` This was inspired by [RectCut](https://halt.software/dead-simple-layouts/), but instead of requiring upfront a fixed pixel size of cut to make, you can have the cut fit to the size of the content. It does this in one pass, without reflow, by "recursing" into the content and asking for its size. This system can also be thought of as a tree of nested columns and rows. - [Visual test with HTML rendering in JSBin](https://jsbin.com/loqapel/edit?js,output) - [TypeScript Playground IDE for the source code](https://www.typescriptlang.org/play?target=1#code/PTAEAsBdIBwZwFwgFZwEYEsB2A6AxgPYC2wANgQI4CGMApqcLQCYaQD8qANAQK6x8AoAQDMeWPJAwEsoAE60sTWrIAUpKgE9ekBKAAym7QEpQAbwGhLoQljiRQ5AOYErVgLygstAO6gAqgCSAURUjrQqAORkBM44MFiOEZygAIwADGnJ6WlGFlY2dqBwtI5ECpDMoB5evoEAyiVlWBVMAMLSkLIEpCoA2hEBzV1JoBEpIxEATBEAurmuBfYVAB72rlWePv4BACq0q5F13hjC9iFYPFSkoABuGD5wDoZ82I6gx5Dg2qBUfC7qWj4ySwLhCjgweGSMB48msBCUyVEpGuNk63VAOExEXm+WkhWKMCosiokAIsg2NW2DUJxNJqhSyQiaFIPFo2KErnkkBhMgB2jiVCYAH0rj10jjLPg+ELSTAVHAeMznvY3AA+PLrIqKvl8KWQIWkWinNQxAjJUw-UgYRxYXQRWURUAAXyMAuFhuN4o16z1MoIcuKpXKzFdhOFspUXs1kuhkBUK0gEtASd9EYJRJJZNy3uAACpvQBBUgVWRYEkYG60UAFgAKAXerC+fGs5AV8keADIG59vnhwFRsK8EByrDrIG6RcjIzkR5ZFlqlYCVU8l6n-SokwrF-y8NKPXGnGazJbrbbRg7nZvtcqJ-vp1ft7rd-q041g0wH2OJxGo1Yt1-Y3jfZE1nFcd2lV8aUzWQk1zYABCdIR5wAMQAeQAOR2IUAHUAgAER2AAJSwPHSAQUIwrDCIAUQCABxQidg2FIADYhBAUBPirVFiQkUBhDJOQFCUVQjF0bwqwqZFQEBTjwCrKgbgHdRmSrY4mE+UAu3k60oFAFRaEbZRQEEtACE+AQOIIYR3nACFwFAEINFANAqwiYRWAAWlRcpsWSKhFBk3hOPoa4eEebx+3sOAMAALyrQEImk1ycFAai3gwMFaAijBkUssA4DwK5JJcDz7GwGKlDkhSlNyqhVNAFgmhivFkkEhMG2kpQSyIbAq1YR4dMcPS0CoYomHykyZC4hsNPAHBJoAaRBbwZAwOAFVoXQxoVMpHnkRRlA3ITuVLR4Yvix5DQ2uSAtAcFKxkRTlPqw0iji7L-MC448o4vBiRgEzmxOUBWFuKR1AqR5PhJARsBLYQqDwKtAmow0mnscxOWEo7nrq1ShXUz5dAuIhXPJAAfUYyu8jpfP82qVMNIUhqgEmeDJ4yqfcryfOabFdAAJVoQVpFIDQAB4LSJ8B2c52RklZnRPA58nnXVRDJsy0JsqKIq3tJfiwYqjAqpmvGmarJqFBa2woXbZQ7gSaqMHJeHZBNvBJrGug+NpKQAG5QZsszNIt161NNzTtMM4b7CJNyab5yARi46b5NBoZPcmi7+secLmFSosSzLSRK10JzXKKJtSCYKbxb14rONK42bPDhrrdsKRbG92E4BCZFjJh9P+qzru8He+K2FAOoXEKq5Xn4pG6U664rm8TRHirwATIhSNhyPUG7AmCHXQaIGB0fKR5Ucv5ozG9RZZB4CQyRUeRRawBuYVIXQ7A9hJkjvyYGLZyMs5bk0ASLYBn9nJK3AcoEwphELY0OqoC2hMo6yxVvLUA3Mk5035gzZSLNY5s2wWrPBvMCEp0Qd6VwHEyo-C3taJhoN7BFRkJFXKVYYAO1kE7N4PtaB+3LAQOho5aD2BlorUhkBxGWBBiodBMsqhuA8DzSAtNmi+S0l2dBStVHqPwdo-mtDozrBUNLTBMjdL2CdBsT460kyuHsfQYowd9LKMwYY6mVCTEp3keY3BVNHFwBwErYAoScAqNVJ4xmJDbGsNJuTIwwAvFzTMUEywBiPD6NkUk1WyhAmuBUR4KJMtcwRKiUrQJrjSDuKxlk2amlcnxJlsUqwOS5LrXCbI3MMtIl2TCe08xyDoxch5MeaREB8ljMsIhTWDCMDLEqErZIKj9iEkUNDFu7COgDi8LIJaK01obVZOJKKHj25vRUetBw2Vob9hkL1ThmD954EPtfAIDQgzaLaB0Lo1xMoX1oBjL5aNQXlHvgsPEnRn50jflAkBbDQWICEh-Buf9Xi9DmGYOZQlUFKLaZg+BlNfGaOTiMPJtjSW4PJVoiopjoXRkUeklpaj6WUsyVkiZpYpkku6WE1gqKcDIAINgSIVhsQ4ENAkT4uY0KYRwvhIiNi466EVVRWiDEmL4pcaANxVZGk8skZMqxc1trErmmqshmqhQ0XooxZ0tTvQLPYmAUa40po-EZhHZp4BkjdWUC83WM0DFeGYIXSaAQbIqLEI-A5zBkgzUit0SSwFQaPFjN66QVYrReAWhxOoNc65g37JWH4jUjS-GLP6zOdgoH7w4stAgq1M2bV0MA7KWB7SNRcO-a4HVvAAxgK8ZIFUKiCgNXYbWUMXJjUqNIUAAAiSAGhR3617iLZdJk27WH7LxEsB8drbD2KsM+IKwXbAhRjZlc5YVPxfqoIByKEy-06K8RB+KDoiSJcQsB5CuacuoVS+JcDANko0Qy3y3L1gcWwPcjAPBd3osHRm4dNBR0ANALwyRwlbpLAzXcqgoBJh4QcBgCgPBTbdgcnAYgCkiC8DvtZFdZHbjdA5rQZdaT36BPnAnEjZShk4ATDKhQjh5V2twgRQiCrKL2u1YxQJhopHWJmbYwJrKrXsqMX4xlKddE+uITkjlUGuV3qCV0u1DqdUdMsKUwVongLibleAeTSqZNEVqQa+p-UbJ-tyhguaPjzMgdg+Yrp1K44FPlvZutuT37ACVhTNIPnDWWfMY5tl4B4tRaSzLVLLrzG8pkOaz4Nq7GuoQu6iAZI4r7OuOmWkgl820EmgAIQUFqRwYQG111CdXXgtcXJVi3LtSoaBnIkY3s5Q2TBiS+DukSTAnQiTOXkBIAKjhDTHqPt82gUEV7AtvpAcFp3LOP3ha-F9MCNPqog5AjFzlCDkFkO+-+jgv3eh-bjHTWDklAbC-40DJnZG0soRS8LmWrClf5Ra4zQXSlmeMQZx009JhpFAJa-96nwPVNmdVzWHyT0GCXJZjiMbqqZ1YBgK4iPLZ1sEgY4jRsVl10BwraqI8wKCHoWAH6a9SCzcGopSSlyVHM-yQFAbGdrmRxCzL+7elBc5gKuAf+ABrVhM0xyZusM2HggNDbZscQkR42AFr89AIRVttBKxuxsjNQckh6fy6Z+SFnjxl2o-KDulQZQAqL1YBEOAavQDIHCvYdQsgwgGpBDwYazcjblTO3CfxRhk3yR53rwXloRejZT9B5onBw9K4nVA6qYAxyh7rUrgxefHBdCW4814jwE44eBjIKb4fnySGdmDI3VurAcXQuZcXJJBX6+wEoOgh074CXJCo+vsjkj0YI7XkE9hJigE1zPsP1vWNjjgFnkkteAAsu-9-6Q8uzyetBETLMqNM2-qzV+gHjQ+xNH5h+WA4jsDOWUTyQ0Ssa4QkOPO5EEWQAeBuV-OuZfQKT-WwNbPqOuJWX-dPP+a7VQQJDiTuW2R4VjeQEIQcZ2d3OAQkZGTOKfMcYccxDiAsR4JQQqD2VyOueqAgSscdPdOA+-d4MaGnV3ZEZyXhTg02JNbnaqXAsAQgWQdsGAaQFgMg31DuTKG2buVeauLXHXDOPXO5Z8D-GAaQ5PU3Oyc3TOIONAYGGyMGDeC3LAWnUUZyYHNHLPbrLiYw93fAjQvPdeTeQvFw3yDA1waEZkCEBnCOYLYmSwTnOlQIwhQJUIq0CeeXBJB7WIyHYvFOUvYwoA-cOEMkJQkkXWIg0FA5Recgyg0eGg5UOg6MBgx5PNZUfXAwo3Ew5sM3RwewthNw9xPPJjCtYBNtGXXIlwGaD2OOYIqwXhCsYoiIgmZYDYTIRIj2JSCoeY5mZyDwZY4wxGTXKsZdX7T7HdVjQIXzSFZoLeZyZdBbKgY4BIHdLiV2ISLbBIa6cdHAWgVKTDYSV4UYmg9aJYDOQgQo7AYo9vQKXOdvKROyPsZPW43wEWOEs4+gC4uRErJFO7TbM7XQAsOQzQKWZYUlZIDQYk-1Mk8DTnZIZOXQG+NE9WDYHFb0CLKwcpbxDYVIl) This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,393 @@ function render(layout: Layout) { const logo = new UIImage('/logo.png', 100, 100) const segmented = new UISegmentedControl(['Intro', '1', '2']) const text = new UIText('Swift manual views layouting without auto layout, no magic, pure code, full control ...') const separator = new UISeparator(1, 'blue') return layout.pad_all(10) .cut_top(sublayout => sublayout.cut_left(logo, { align: 'top' }).pad_left(10) .cut_top(segmented).pad_top(10) .put(text) ) .cut_top(separator) /* Alternative API without closures & without chaining: layout.pad_all(10) const sublayout = layout.cut_top() sublayout.cut_left(logo, { align: 'top' }) sublayout.pad_left(10) sublayout.cut_top(segmented) sublayout.pad_top(10) sublayout.put(text) layout.cut_top(separator) */ } const FONT_WIDTH = 10 const FONT_HEIGHT = 16 // the contract for render(): we tell you the available width & height (either or both // of which may be 'fit-content'), and you tell us what size you'll be. Eg images will // scale to fit inside the available dimensions, or text will determine its height based // on the width. // Known issue: assumes render() returns sizes less than given available sizes, and will // crap out if it violates that interface UIElement { render(available_width: number | 'fit-content', available_height: number | 'fit-content'): Readonly<{ width: number, height: number }> } // images scale to fit inside the available dimensions, preserving their intrinsic // aspect ratio; if both available width & height are 'fit-content', then the intrinsic // size is used. Alternative: maybe should only scale to fit if available dimensions // are smaller than the intrinsic size? So scaling factor will always be ≤1? class UIImage implements UIElement { constructor(readonly url: string, readonly width: number, readonly height: number) {} render(avail_width: number | 'fit-content', avail_height: number | 'fit-content') { // fit as big as it can while preserving aspect ratio let width, height if (avail_width === 'fit-content' && avail_height === 'fit-content') { ({ width, height } = this) } else if (avail_width === 'fit-content' || this.height/this.width > (avail_height as number)/avail_width) { height = avail_height as number width = this.width*height/this.height } else { width = avail_width height = this.height*width/this.width } return { width, height } } } // fixed height, width expands to fit container // Known issue: what if available width is less than min width? class UISegmentedControl implements UIElement { constructor(readonly items: readonly string[]) {} render(avail_width: number | 'fit-content', avail_height: number | 'fit-content') { if (avail_width === 'fit-content') { return { width: this.items.join(' ').length*FONT_WIDTH, height: FONT_HEIGHT } } else { return { width: avail_width, height: FONT_HEIGHT } } } } // based on available width, determines the height needed. // If width unconstrained, the whole text is put on one line. // Should it have a default width instead? // Known issue: doesn't do real text wrapping, instead estimates based on "typical // area" of a character class UIText implements UIElement { constructor(readonly text: string) {} render(avail_width: number | 'fit-content', avail_height: number | 'fit-content') { // in leiu of real text wrapping, pretend that text is a 2D liquid with some amount of "2D volume"/area const area = this.text.length*FONT_WIDTH*FONT_HEIGHT let width, height if (avail_width === 'fit-content' && avail_height === 'fit-content') { height = FONT_HEIGHT width = this.text.length*FONT_WIDTH } else if (avail_width === 'fit-content') { height = avail_height as number width = area/height|0 } else { width = avail_width height = area/width|0 } return { width, height } } } // horizontal separator line // Ben suggested this should be subsumed by a way to draw an arbitrary rectangle class UISeparator implements UIElement { constructor(readonly height: number, readonly color: string) {} render(avail_width: number | 'fit-content', avail_height: number | 'fit-content') { return { width: avail_width === 'fit-content' ? 20 : avail_width, height: this.height } } } class Layout { // If the initial available width or height is a fixed number, then the layout // will always have that width or height and the available width and height will // shrink as the layout is cut up to put things in. // However if the initial available width or height is "fit-content" (meaning it's // just large enough to fit its content), then the layout will always be fit-content, // and instead the/ layout's width and height will grow as things are put in by // cutting it up. // Note that this is independent for width and height, so that's not 2 kinds // of layouts, that's 4 kinds (fixed size, fixed width, fixed height, unconstrained). // The top-level page is normally fixed width and unconstrained height. constructor( // dimensions of remaining available space in this layout: // As described above, if a fixed size was initially provided, then the // corresponding available dimension will shrink as the layout is cut up // to put things in; but if it was initially 'fit-content', then the // available dimension will always be 'fit-content'. public available_width: number | 'fit-content', public available_height: number | 'fit-content', // top-left coordinates of remaining available space in this layout: // As the layout is cut up to put things in it, these will move down and // to the right. private available_x = 0, private available_y = 0, // fake "rendering" of UI elements by "drawing" their rectangles, i.e. appending // to this list the coordinates and sizes at which to draw each UI element readonly rects: Array<{x: number, y: number, width: number, height: number, content: UIElement }> = [] ) { this.width = available_width === 'fit-content' ? 0 : available_width this.height = available_height === 'fit-content' ? 0 : available_height this.x = this.available_x this.y = this.available_y } // the outer dimensions of this layout: // As described above, if the initial available width or height was a fixed // number, then this width/height will always be that value; but if the layout // is fit-content along a dimension, then the width or height will grow as // content is put into the layout by cutting it up. width: number height: number // the initial top-left coordinates of the layout pane; never changes private readonly x: number private readonly y: number // cut out padding along a side pad_top(padding: number) { this.available_y += padding return this.pad_bottom(padding) } pad_bottom(padding: number) { if (this.available_height === 'fit-content') { this.height += padding } else { this.available_height -= padding } return this } pad_left(padding: number) { this.available_x += padding return this.pad_right(padding) } pad_right(padding: number) { if (this.available_width === 'fit-content') { this.width += padding } else { this.available_width -= padding } return this } pad_all(v_padding: number, h_padding?: number) { h_padding ??= v_padding this.available_x += h_padding this.available_y += v_padding return this.pad_bottom(2*v_padding).pad_right(2*h_padding) } // cut out a row at the top, put the given content in there cut_top(content: UIElement | ((l: Layout) => Layout), { align }: { align: 'left' | 'center' | 'right' } = { align: 'center' }) { // render the content, calculating the x/y/width/height and "drawing" it by // adding it to the rects array let rect if (content instanceof Function) { rect = content(new Layout(this.available_width, 'fit-content', this.available_x, this.available_y, this.rects)) } else { rect = content.render(this.available_width, 'fit-content') this.rects.push({ x: this.h_align(align, rect.width), y: this.available_y, ...rect, content }) } // when cutting out a row at the top, the x-coord of the remaining available // space doesn't change, but the y-coord grows this.available_y += rect.height // if the layout is fixed-width, that doesn't change; // if the width is fit-content, then the width grows if this newly drawn // rect would stick out the side of the layout if (this.available_width === 'fit-content') { this.width = Math.max(this.width, this.available_x - this.x + rect.width) } // if the layout is fixed-height, that doesn't change, but the remaining // available width shrinks; // if the height is fit-content, then the height grows if this newly drawn // rect sticks out the bottom of the layout if (this.available_height !== 'fit-content') { this.available_height -= rect.height } else { this.height = Math.max(this.height, this.available_y - this.y) } return this } // cut out a row at the bottom, put the given content in there cut_bottom(content: UIElement | ((l: Layout) => Layout), { align }: { align: 'left' | 'center' | 'right' } = { align: 'center' }) { // can't put content at the bottom of a variable-height layout because we'd // have to recalculate the y-coords as the height of the layout grows if (this.available_height === 'fit-content') throw 'Can only cut-bottom of layouts with fixed height' // render the content, calculating the x/y/width/height and "drawing" it by // adding it to the rects array let rect if (content instanceof Function) { rect = content(new Layout(this.available_width, 'fit-content', this.available_x, this.available_y, this.rects)) } else { rect = content.render(this.available_width, 'fit-content') const y = this.available_y + this.available_height - rect.height this.rects.push({ x: this.h_align(align, rect.width), y, ...rect, content }) } // cutting out a row at the bottom doesn't change the x or y-coord of the // remaining available space, but does shrink the height this.available_height -= rect.height // if the layout is fixed-width, that doesn't change; // if the width is fit-content, then the width grows if this newly drawn // rect would stick out the side of the layout if (this.available_width === 'fit-content') { this.width = Math.max(this.width, this.available_x - this.x + rect.width) } return this } // cut out a column on the left, put the given content in there cut_left(content: UIElement | ((l: Layout) => Layout), { align }: { align: 'top' | 'center' | 'bottom' } = { align: 'center' }) { // render the content, calculating the x/y/width/height and "drawing" it by // adding it to the rects array let rect if (content instanceof Function) { rect = content(new Layout('fit-content', this.available_height, this.available_x, this.available_y, this.rects)) } else { rect = content.render('fit-content', this.available_height) this.rects.push({ x: this.available_x, y: this.v_align(align, rect.height), ...rect, content }) } // when cutting out a column on the left, the y-coord of the remaining // available space doesn't change, the x-coord grows this.available_x += rect.width // if the layout is fixed-width, that doesn't change, but the remaining // available width shrinks; // if the width is fit-content, then the width grows if this newly drawn // rect sticks out the side of the layout if (this.available_width !== 'fit-content') { this.available_width -= rect.width } else { this.width = Math.max(this.width, this.available_x - this.x) } // if the layout is fixed-height, that doesn't change; // if the height is fit-content, then the height grows if this newly drawn // rect would stick out the bottom of the layout if (this.available_height === 'fit-content') { this.height = Math.max(this.height, this.available_y + rect.height - this.y) } return this } // cut out a column on the right, put the given content in there cut_right(content: UIElement | ((l: Layout) => Layout), { align }: { align: 'top' | 'center' | 'bottom' } = { align: 'center' }) { // can't put content on the right edge of a variable-width layout because we'd // have to recalculate the x-coords as the width of the layout grows if (this.available_width === 'fit-content') throw 'Can only cut right of layouts with fixed width' // render the content, calculating the x/y/width/height and "drawing" it by // adding it to the rects array let rect if (content instanceof Function) { rect = content(new Layout('fit-content', this.available_height, this.available_x, this.available_y, this.rects)) } else { rect = content.render('fit-content', this.available_height) const x = this.available_x + this.available_width - rect.width this.rects.push({ x, y: this.v_align(align, rect.height), ...rect, content }) } // cutting out a column on the right doesn't change the x or y-coord of the // remaining available space, but does shrink the width this.available_width -= rect.width // if the layout is fixed-height, that doesn't change; // if the height is fit-content, then the height grows if this newly drawn // rect would stick out the bottom of the layout if (this.available_height === 'fit-content') { this.height = Math.max(this.height, this.available_y + rect.height - this.y) } return this } // put the given item in the remaining space in the layout put(content: UIElement, { align }: { align: 'top' | 'left' | 'center' | 'right' | 'bottom' | `${'top' | 'bottom'}-${'left' | 'right'}` } = { align: 'center' }) { // parse the alignment specifier into h-align and v-align specifiers let v_align: 'top' | 'center' | 'bottom', h_align: 'left' | 'center' | 'right' if (align === 'top' || align === 'bottom') [v_align, h_align] = [align, 'center'] else if (align === 'left' || align === 'right') [v_align, h_align] = ['center', align] else if (align === 'center') [v_align, h_align] = ['center', 'center'] else [v_align, h_align] = align.split('-') as ['top' | 'bottom', 'left' | 'right'] // render the content, calculating the x/y/width/height and "drawing" it by // adding it to the rects array const rect = content.render(this.available_width, this.available_height) this.rects.push({ x: this.h_align(h_align, rect.width), y: this.v_align(v_align, rect.height), ...rect, content, }) // if the layout is fixed-width, that doesn't change, but the remaining // available width is used up; // if the width is fit-content, then the width grows if this newly drawn // rect sticks out the side of the layout if (this.available_width !== 'fit-content') { this.available_width = 0 } else { this.width = Math.max(this.width, this.available_x - this.x + rect.width) } // if the layout is fixed-height, that doesn't change, but the remaining // available height is used up; // if the height is fit-content, then the height grows if this newly drawn // rect sticks out the bottom of the layout if (this.available_height !== 'fit-content') { this.available_height = 0 } else { this.height = Math.max(this.height, this.available_y - this.y + rect.height) } return this } // calculates the x-coord of a rect with given width & alignment in remaining available space h_align(align: 'left' | 'center' | 'right', width: number) { return align === 'left' || this.available_width === 'fit-content' ? this.available_x : align === 'right' ? this.available_x + this.available_width - width : /* center */ this.available_x + (this.available_width - width)/2 | 0 } // calculates the y-coord of a rect with given height & alignment in remaining available space v_align(align: 'top' | 'center' | 'bottom', height: number) { return align === 'top' || this.available_height === 'fit-content' ? this.available_y : align === 'bottom' ? this.available_y + this.available_height - height : /* center */ this.available_y + (this.available_height - height)/2 | 0 } } // test: const l = render(new Layout(800, 'fit-content')) for (const rect of l.rects) { const el = document.createElement('div') el.style.position = 'absolute' el.style.left = rect.x + 'px' el.style.top = rect.y + 'px' el.style.width = rect.width + 'px' el.style.height = rect.height + 'px' el.style.background = 'skyblue' el.textContent = rect.content.constructor.name document.body.appendChild(el) } // TypeScript won't assume this for ES5 output for some reason? declare interface Function { name: string }