From: Thomas Walker Lynch Date: Fri, 7 Aug 2026 06:52:18 +0000 (+0000) Subject: shrink wrap text then place grid cell entries X-Git-Url: https://git.reasoningtechnology.com/%27%20%20%20window.RT.dirpr_library%20%20%20%27/%27%20%20%20key%20%20%20%27?a=commitdiff_plain;h=9db4745151c3c07972635990d9549499a0d6f80d;p=RT-Style shrink wrap text then place grid cell entries --- diff --git a/developer/authored/Manuscript.copy/Core/utility.js b/developer/authored/Manuscript.copy/Core/utility.js index 5b00409..42ea4af 100644 --- a/developer/authored/Manuscript.copy/Core/utility.js +++ b/developer/authored/Manuscript.copy/Core/utility.js @@ -216,6 +216,108 @@ window.RT.Utility = { // DOM Structural Operations window.RT.Utility.Dom = window.RT.Utility.Dom || {}; + + /* Shrink wrap an attached element to a well set block of text. + + Text left in a box as wide as its column wraps wherever the width happens + to run out, which strands a word or two on the last line: + + One two three and then some + more + + Two steps fix it, and both are needed. First narrow the box until narrowing + it further would cost another line. Every line then carries text: + + One two three and + then some more + + Second, hug the longest line. Until this is done the box is still as wide as + it was allowed to be, and a box wider than its text cannot be placed: pushed + against the right edge of a cell it looks exactly as it did against the + left, because the lines inside have not moved. + + Afterwards the longest line touches the right edge of the box, the text is + flush left with its ragged edge on the right where a reader expects it, and + the box as a whole can be placed wherever it belongs. + + The element must be attached and laid out, since every question here is one + only the browser can answer. Widths are searched by bisection rather than + stepped, which costs about ten measurements instead of one per em. + */ + const line_metrics = function(el){ + const range = document.createRange(); + range.selectNodeContents(el); + const rects = range.getClientRects(); + const lines = new Map(); + for(let i = 0; i < rects.length; i++){ + const r = rects[i]; + if(r.width === 0 && r.height === 0) continue; + const key = Math.round(r.top); + const cur = lines.get(key); + if(cur){ + if(r.left < cur.left) cur.left = r.left; + if(r.right > cur.right) cur.right = r.right; + }else{ + lines.set(key ,{ left: r.left ,right: r.right }); + } + } + let widest = 0; + lines.forEach(l => { const w = l.right - l.left; if(w > widest) widest = w; }); + return { count: lines.size || 1 ,widest: widest }; + }; + + window.RT.Utility.Dom.line_metrics = line_metrics; + + window.RT.Utility.Dom.shrink_wrap = function(el ,options){ + options = options || {}; + if(!el || !el.isConnected) return el; + + /* The ceiling is the parent's content box. clientWidth includes padding , + so using it directly would allow the label to be set wider than the space + it actually occupies. */ + const parent = el.parentElement; + let parent_content = 0; + if(parent){ + const ps = window.getComputedStyle(parent); + parent_content = parent.clientWidth + - parseFloat(ps.paddingLeft || 0) + - parseFloat(ps.paddingRight || 0); + } + const max_width = options.max_width || parent_content || el.offsetWidth; + if(!(max_width > 0)) return el; + + el.style.display = 'inline-block'; + el.style.textAlign = 'left'; + el.style.width = max_width + 'px'; + + let m = line_metrics(el); + const target_lines = m.count; + + if(target_lines > 1){ + // The box never needs to exceed the widest line it already produced. + let lo = 1; + let hi = Math.ceil(m.widest) || max_width; + let best = hi; + while(hi - lo > 2){ + const mid = Math.floor((lo + hi) / 2); + el.style.width = mid + 'px'; + if(line_metrics(el).count <= target_lines){ + best = mid; + hi = mid; + }else{ + lo = mid + 1; + } + } + el.style.width = best + 'px'; + } + + // Hug the longest line, so it touches the right edge of the box. + m = line_metrics(el); + if(m.widest > 0) el.style.width = Math.ceil(m.widest) + 'px'; + + return el; + }; + window.RT.Utility.Dom.get_structural_depth = function(element, counter_name) { let depth = 0; let curr = element.parentElement; diff --git a/developer/authored/Manuscript.copy/Document/design.html b/developer/authored/Manuscript.copy/Document/design.html index 0509b97..0978f88 100644 --- a/developer/authored/Manuscript.copy/Document/design.html +++ b/developer/authored/Manuscript.copy/Document/design.html @@ -842,6 +842,18 @@

Line lengths within the label are evened so that no line is left holding a single stranded word, which is what makes a narrow heading column look broken. Where a browser does not implement the evening, the text still wraps and only the evenness is lost.

+

+ Setting the label takes two narrowings, and both are necessary. Text left in a box as wide as its column wraps wherever the width happens to run out, which strands a word or two on a line of their own. The first narrowing reduces the box until reducing it further would cost another line; every line then carries text, and the block reads as a block rather than as a sentence that ran out of room. +

+

+ The second narrowing hugs the longest line. Until it is done the box is still as wide as it was permitted to be, and a box wider than its text cannot be placed: pressed against the right edge of its cell it looks exactly as it did against the left, because the lines within it have not moved. This is the likeliest explanation when placement appears to have been ignored altogether. +

+

+ Afterwards the longest line touches the right edge of the box, the text is flush left with its ragged edge on the right where a reader expects it, and the box can be placed wherever it belongs. +

+

+ Neither width can be written in a style sheet, because neither is known until the lines exist. Both are asked of the browser once they do: a range over the contents reports one rectangle per line box, which gives the line count and the widest line. The search bisects rather than stepping, costing about ten measurements rather than one per em. The work is not particular to grids, so it lives in the utilities as shrink_wrap and is available to any element that sets a label. +

diff --git a/developer/authored/Manuscript.copy/Element/grid.js b/developer/authored/Manuscript.copy/Element/grid.js index 67ef078..ce18eb4 100644 --- a/developer/authored/Manuscript.copy/Element/grid.js +++ b/developer/authored/Manuscript.copy/Element/grid.js @@ -45,7 +45,6 @@ inner.className = 'RT_grid_label'; inner.style.display = 'inline-block'; inner.style.textAlign = 'left'; - inner.style.textWrap = 'balance'; inner.style.maxWidth = '100%'; while(el.firstChild) inner.appendChild(el.firstChild); el.appendChild(inner); @@ -200,6 +199,7 @@ container_node.replaceWith(wrapper); freeze_columns(wrapper); + shrink_labels(wrapper); execute_two_pass_measurement(wrapper, options); } @@ -254,6 +254,7 @@ container_node.replaceWith(wrapper); freeze_columns(wrapper); + shrink_labels(wrapper); execute_two_pass_measurement(wrapper, options); } @@ -392,6 +393,18 @@ value is written to the inline style, so it survives cloneNode and travels with fragments that have been detached from the document. */ + /* Size every composed label. The work belongs to the utility ,since nothing + about it is particular to grids; a label in any element wants the same + treatment. Run after the columns are frozen ,so the wrapping measured is + the wrapping that will be rendered. */ + function shrink_labels(wrapper){ + if(!wrapper || !wrapper.isConnected) return; + const labels = wrapper.querySelectorAll('.RT_grid_label'); + for(let i = 0; i < labels.length; i++){ + window.RT.Utility.Dom.shrink_wrap(labels[i]); + } + } + function freeze_columns(wrapper){ if(!wrapper || !wrapper.isConnected) return; const resolved = window.getComputedStyle(wrapper).gridTemplateColumns;