druid_widget_nursery/table/
table_column_width.rs

1// Copyright 2021 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// Author: Dietmar Maurer <dietmar@proxmox.com>
16
17use std::ops::Range;
18
19/// Simple table column width specification (without minimin and maximum).
20///
21/// If you need minimum/maximum settings use [`ComplexTableColumnWidth`].
22#[derive(Copy, Clone, Debug)]
23pub enum TableColumnWidth {
24    /// Fixed size
25    Fixed(f64),
26    /// Flex layout algorithm.
27    Flex(f64),
28    /// Use a fraction of the total table width.
29    Fraction(f64),
30    /// Maximum of the dimensions of all cells in a column.
31    Intrinsic,
32}
33
34impl TableColumnWidth {
35    fn resolve_width(
36        &self,
37        total_width: f64,
38        intrinsic_width: f64,
39        px_per_flex: f64,
40    ) -> (f64, f64, f64) {
41        match self {
42            Self::Fixed(w) => (*w, *w, 0f64),
43            Self::Flex(f) => (f * px_per_flex, 0f64, *f),
44            Self::Fraction(f) => {
45                let w = f * total_width;
46                (w, w, 0f64)
47            }
48            Self::Intrinsic => (intrinsic_width, intrinsic_width, 0f64),
49        }
50    }
51
52    fn need_intrinsic_width(&self) -> bool {
53        matches!(self, Self::Intrinsic)
54    }
55}
56
57impl From<f64> for TableColumnWidth {
58    fn from(fixed_width: f64) -> Self {
59        TableColumnWidth::Fixed(fixed_width)
60    }
61}
62
63/// Table column width with optional minimum and maximum limits.
64///
65/// ```
66/// use druid_widget_nursery::table::{ComplexTableColumnWidth, TableColumnWidth::*};
67///
68/// // flex layout, but with minimum and maximum size
69/// ComplexTableColumnWidth::with_min_max(Flex(1.), Fixed(100.), Fixed(300.));
70///
71/// // flex layout, but use intrinsic size a minimum
72/// ComplexTableColumnWidth::with_min(Flex(1.), Intrinsic);
73/// ```
74///
75/// It is usually not necessary to use this type directly, because
76/// thert are function to convert from:
77///
78/// - f64 => Simple(Fixed(f64))
79/// - `Into<TableColumnWidth>` => Simple(TableColumnWidth))
80/// - `(Into<TableColumnWidth>`, `Range<f64>`) => Limited with min/max from range
81/// - `(Into<TableColumnWidth>`, `Into<TableColumnWidth>`) => Limited with minimun
82/// - `(Into<TableColumnWidth>`, `Into<TableColumnWidth>`, `Into<TableColumnWidth>`) => Limited with min/max
83///
84/// Examples:
85/// ```
86/// use druid_widget_nursery::table::{FlexTable, TableColumnWidth::*};
87/// # fn test () -> FlexTable<()> {
88/// FlexTable::new()
89///    .with_column_width(64.0)
90///    .with_column_width(Intrinsic)
91///    .with_column_width((Flex(1.0), 60.0))
92///    .with_column_width((Flex(1.0), 60.0..200.0))
93/// # }
94/// ```
95#[derive(Copy, Clone, Debug)]
96pub enum ComplexTableColumnWidth {
97    /// Column without limits
98    Simple(TableColumnWidth),
99    /// Limited column (width, min, max)
100    ///
101    /// It is usually better to avoid flex dependent 'min' and 'max'
102    /// constraint, because it can lead to unexpected results (with
103    /// the current resolver).
104    Limited(TableColumnWidth, TableColumnWidth, TableColumnWidth),
105}
106
107impl<W: Into<TableColumnWidth>> From<W> for ComplexTableColumnWidth {
108    fn from(tcw: W) -> Self {
109        let tcw: TableColumnWidth = tcw.into();
110        Self::simple(tcw)
111    }
112}
113
114impl<W: Into<TableColumnWidth>> From<(W, Range<f64>)> for ComplexTableColumnWidth {
115    fn from(data: (W, Range<f64>)) -> Self {
116        let tcw: TableColumnWidth = data.0.into();
117        let min = TableColumnWidth::Fixed(data.1.start);
118        let max = TableColumnWidth::Fixed(data.1.end);
119        Self::with_min_max(tcw, min, max)
120    }
121}
122
123impl<W1, W2> From<(W1, W2)> for ComplexTableColumnWidth
124where
125    W1: Into<TableColumnWidth>,
126    W2: Into<TableColumnWidth>,
127{
128    fn from(data: (W1, W2)) -> Self {
129        let tcw: TableColumnWidth = data.0.into();
130        let min: TableColumnWidth = data.1.into();
131        let max = TableColumnWidth::Fixed(0.0);
132        Self::with_min_max(tcw, min, max)
133    }
134}
135
136impl<W1, W2, W3> From<(W1, W2, W3)> for ComplexTableColumnWidth
137where
138    W1: Into<TableColumnWidth>,
139    W2: Into<TableColumnWidth>,
140    W3: Into<TableColumnWidth>,
141{
142    fn from(data: (W1, W2, W3)) -> Self {
143        let tcw: TableColumnWidth = data.0.into();
144        let min: TableColumnWidth = data.1.into();
145        let max: TableColumnWidth = data.2.into();
146        Self::with_min_max(tcw, min, max)
147    }
148}
149
150impl ComplexTableColumnWidth {
151    /// Create instance without limits
152    pub fn simple(tcw: TableColumnWidth) -> Self {
153        Self::Simple(tcw)
154    }
155
156    /// Create instance with minimum limit
157    pub fn with_min<W1, W2>(tcw: W1, min: W2) -> Self
158    where
159        W1: Into<TableColumnWidth>,
160        W2: Into<TableColumnWidth>,
161    {
162        let tcw: TableColumnWidth = tcw.into();
163        let min: TableColumnWidth = min.into();
164        Self::with_min_max(tcw, min, TableColumnWidth::Fixed(0f64))
165    }
166
167    /// Create instance with maximum limit
168    pub fn with_max<W1, W2>(tcw: W1, max: W2) -> Self
169    where
170        W1: Into<TableColumnWidth>,
171        W2: Into<TableColumnWidth>,
172    {
173        let tcw: TableColumnWidth = tcw.into();
174        let max: TableColumnWidth = max.into();
175        Self::with_min_max(tcw, TableColumnWidth::Fixed(0f64), max)
176    }
177
178    /// Create instance with minimum and maximum limit
179    pub fn with_min_max<W1, W2, W3>(tcw: W1, min: W2, max: W3) -> Self
180    where
181        W1: Into<TableColumnWidth>,
182        W2: Into<TableColumnWidth>,
183        W3: Into<TableColumnWidth>,
184    {
185        let tcw: TableColumnWidth = tcw.into();
186        let min: TableColumnWidth = min.into();
187        let max: TableColumnWidth = max.into();
188        Self::Limited(tcw, min, max)
189    }
190
191    pub(crate) fn need_intrinsic_width(&self) -> bool {
192        match self {
193            Self::Simple(tcw) => tcw.need_intrinsic_width(),
194            Self::Limited(tcw, min, max) => {
195                tcw.need_intrinsic_width()
196                    || min.need_intrinsic_width()
197                    || max.need_intrinsic_width()
198            }
199        }
200    }
201
202    fn resolve_width(
203        &self,
204        total_width: f64,
205        intrinsic_width: f64,
206        px_per_flex: f64,
207        apply_limits: bool,
208    ) -> (bool, f64, f64, f64) {
209        match self {
210            Self::Simple(tcw) => {
211                let (col_width, col_fixed, col_flex) =
212                    tcw.resolve_width(total_width, intrinsic_width, px_per_flex);
213                (false, col_width, col_fixed, col_flex)
214            }
215            Self::Limited(tcw, min, max) => {
216                let (col_width, col_fixed, col_flex) =
217                    tcw.resolve_width(total_width, intrinsic_width, px_per_flex);
218
219                if apply_limits {
220                    let (min_width, _min_fixed, _min_flex) =
221                        min.resolve_width(total_width, intrinsic_width, px_per_flex);
222                    let (max_width, _max_fixed, _max_flex) =
223                        max.resolve_width(total_width, intrinsic_width, px_per_flex);
224
225                    if (max_width > 0f64) && (col_width > max_width) {
226                        return (true, max_width, max_width, 0f64);
227                    }
228                    if (min_width > 0f64) && (col_width < min_width) {
229                        return (true, min_width, min_width, 0f64);
230                    }
231                }
232                (false, col_width, col_fixed, col_flex)
233            }
234        }
235    }
236
237    /// We may use this for several table widget implementations
238    pub(crate) fn compute_column_widths(
239        column_widths: &mut [ComplexTableColumnWidth],
240        intrinsic_widths: &[f64],
241        max_table_width: f64,
242    ) -> Vec<f64> {
243        let column_count = column_widths.len();
244        let mut col_widths = vec![0f64; column_count];
245
246        // Note: This needs only one step unless you use MIN/MAX
247        // column constraints
248        'outer: for _ in 0..column_count {
249            // Step 1: Compute pixels per flex
250
251            let px_per_flex = {
252                let mut fixed_width = 0f64;
253                let mut flex_sum = 0f64;
254
255                for col_num in 0..column_count {
256                    let (_, _width, fixed_share, flex_share) = column_widths[col_num]
257                        .resolve_width(max_table_width, intrinsic_widths[col_num], 0f64, false);
258                    fixed_width += fixed_share;
259                    flex_sum += flex_share;
260                }
261
262                let remaining = (max_table_width - fixed_width).max(0.0);
263                if flex_sum != 0f64 {
264                    remaining / flex_sum
265                } else {
266                    0f64
267                }
268            };
269
270            // Step 2: Apply min/max limits
271
272            for col_num in 0..column_count {
273                let (limit, width, _fixed_share, _flex_share) = column_widths[col_num]
274                    .resolve_width(
275                        max_table_width,
276                        intrinsic_widths[col_num],
277                        px_per_flex,
278                        true,
279                    );
280                col_widths[col_num] = width;
281
282                if limit {
283                    // convert into a fixed size column
284                    column_widths[col_num] = TableColumnWidth::Fixed(width).into();
285                    continue 'outer;
286                }
287            }
288
289            break; // done
290        }
291
292        col_widths
293    }
294}