Table resize (#1438)
* Let 1D strips fill up parent width/height * Add Strip + Table + DatePicker to egui_extras changelog * Expose some dragging- and pointer related context/memory methods * Make tables resizable
This commit is contained in:
parent
a52bbade45
commit
21c32a18d8
12 changed files with 323 additions and 159 deletions
|
@ -201,7 +201,7 @@ impl SidePanel {
|
||||||
let mut is_resizing = false;
|
let mut is_resizing = false;
|
||||||
if resizable {
|
if resizable {
|
||||||
let resize_id = id.with("__resize");
|
let resize_id = id.with("__resize");
|
||||||
if let Some(pointer) = ui.ctx().latest_pointer_pos() {
|
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
|
||||||
let we_are_on_top = ui
|
let we_are_on_top = ui
|
||||||
.ctx()
|
.ctx()
|
||||||
.layer_id_at(pointer)
|
.layer_id_at(pointer)
|
||||||
|
@ -217,9 +217,9 @@ impl SidePanel {
|
||||||
&& ui.input().pointer.any_down()
|
&& ui.input().pointer.any_down()
|
||||||
&& mouse_over_resize_line
|
&& mouse_over_resize_line
|
||||||
{
|
{
|
||||||
ui.memory().interaction.drag_id = Some(resize_id);
|
ui.memory().set_dragged_id(resize_id);
|
||||||
}
|
}
|
||||||
is_resizing = ui.memory().interaction.drag_id == Some(resize_id);
|
is_resizing = ui.memory().is_being_dragged(resize_id);
|
||||||
if is_resizing {
|
if is_resizing {
|
||||||
let width = (pointer.x - side.side_x(panel_rect)).abs();
|
let width = (pointer.x - side.side_x(panel_rect)).abs();
|
||||||
let width =
|
let width =
|
||||||
|
|
|
@ -885,7 +885,7 @@ impl Context {
|
||||||
/// Latest reported pointer position.
|
/// Latest reported pointer position.
|
||||||
/// When tapping a touch screen, this will be `None`.
|
/// When tapping a touch screen, this will be `None`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub(crate) fn latest_pointer_pos(&self) -> Option<Pos2> {
|
pub fn pointer_latest_pos(&self) -> Option<Pos2> {
|
||||||
self.input().pointer.latest_pos()
|
self.input().pointer.latest_pos()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo
|
||||||
response
|
response
|
||||||
.on_hover_cursor(CursorIcon::ZoomIn)
|
.on_hover_cursor(CursorIcon::ZoomIn)
|
||||||
.on_hover_ui_at_pointer(|ui| {
|
.on_hover_ui_at_pointer(|ui| {
|
||||||
if let Some(pos) = ui.ctx().latest_pointer_pos() {
|
if let Some(pos) = ui.ctx().pointer_latest_pos() {
|
||||||
let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0));
|
let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0));
|
||||||
let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w);
|
let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w);
|
||||||
let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h);
|
let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h);
|
||||||
|
|
|
@ -418,6 +418,11 @@ impl Memory {
|
||||||
self.interaction.drag_id == Some(id)
|
self.interaction.drag_id == Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn set_dragged_id(&mut self, id: Id) {
|
||||||
|
self.interaction.drag_id = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Forget window positions, sizes etc.
|
/// Forget window positions, sizes etc.
|
||||||
/// Can be used to auto-layout windows.
|
/// Can be used to auto-layout windows.
|
||||||
pub fn reset_areas(&mut self) {
|
pub fn reset_areas(&mut self) {
|
||||||
|
|
|
@ -26,13 +26,10 @@ impl super::Demo for StripDemo {
|
||||||
impl super::View for StripDemo {
|
impl super::View for StripDemo {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.size(Size::Absolute(50.0))
|
.size(Size::exact(50.0))
|
||||||
.size(Size::Remainder)
|
.size(Size::remainder())
|
||||||
.size(Size::RelativeMinimum {
|
.size(Size::relative(0.5).at_least(60.0))
|
||||||
relative: 0.5,
|
.size(Size::exact(10.0))
|
||||||
minimum: 60.0,
|
|
||||||
})
|
|
||||||
.size(Size::Absolute(10.0))
|
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
strip.cell_clip(|ui| {
|
strip.cell_clip(|ui| {
|
||||||
ui.painter()
|
ui.painter()
|
||||||
|
@ -40,7 +37,7 @@ impl super::View for StripDemo {
|
||||||
ui.label("Full width and 50px height");
|
ui.label("Full width and 50px height");
|
||||||
});
|
});
|
||||||
strip.strip(|builder| {
|
strip.strip(|builder| {
|
||||||
builder.sizes(Size::Remainder, 2).horizontal(|mut strip| {
|
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||||
strip.cell_clip(|ui| {
|
strip.cell_clip(|ui| {
|
||||||
ui.painter().rect_filled(
|
ui.painter().rect_filled(
|
||||||
ui.available_rect_before_wrap(),
|
ui.available_rect_before_wrap(),
|
||||||
|
@ -50,7 +47,7 @@ impl super::View for StripDemo {
|
||||||
ui.label("remaining height and 50% of the width");
|
ui.label("remaining height and 50% of the width");
|
||||||
});
|
});
|
||||||
strip.strip(|builder| {
|
strip.strip(|builder| {
|
||||||
builder.sizes(Size::Remainder, 3).vertical(|mut strip| {
|
builder.sizes(Size::remainder(), 3).vertical(|mut strip| {
|
||||||
strip.empty();
|
strip.empty();
|
||||||
strip.cell_clip(|ui| {
|
strip.cell_clip(|ui| {
|
||||||
ui.painter().rect_filled(
|
ui.painter().rect_filled(
|
||||||
|
@ -66,17 +63,17 @@ impl super::View for StripDemo {
|
||||||
});
|
});
|
||||||
strip.strip(|builder| {
|
strip.strip(|builder| {
|
||||||
builder
|
builder
|
||||||
.size(Size::Remainder)
|
.size(Size::remainder())
|
||||||
.size(Size::Absolute(60.0))
|
.size(Size::exact(60.0))
|
||||||
.size(Size::Remainder)
|
.size(Size::remainder())
|
||||||
.size(Size::Absolute(70.0))
|
.size(Size::exact(70.0))
|
||||||
.horizontal(|mut strip| {
|
.horizontal(|mut strip| {
|
||||||
strip.empty();
|
strip.empty();
|
||||||
strip.strip(|builder| {
|
strip.strip(|builder| {
|
||||||
builder
|
builder
|
||||||
.size(Size::Remainder)
|
.size(Size::remainder())
|
||||||
.size(Size::Absolute(60.0))
|
.size(Size::exact(60.0))
|
||||||
.size(Size::Remainder)
|
.size(Size::remainder())
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
strip.empty();
|
strip.empty();
|
||||||
strip.cell_clip(|ui| {
|
strip.cell_clip(|ui| {
|
||||||
|
|
|
@ -5,6 +5,7 @@ use egui_extras::{Size, StripBuilder, TableBuilder};
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TableDemo {
|
pub struct TableDemo {
|
||||||
virtual_scroll: bool,
|
virtual_scroll: bool,
|
||||||
|
resizable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Demo for TableDemo {
|
impl super::Demo for TableDemo {
|
||||||
|
@ -26,12 +27,13 @@ impl super::Demo for TableDemo {
|
||||||
|
|
||||||
impl super::View for TableDemo {
|
impl super::View for TableDemo {
|
||||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.checkbox(&mut self.virtual_scroll, "Virtual scroll demo");
|
ui.checkbox(&mut self.virtual_scroll, "Virtual scroll");
|
||||||
|
ui.checkbox(&mut self.resizable, "Resizable columns");
|
||||||
|
|
||||||
// Leave room for the source code link after the table demo:
|
// Leave room for the source code link after the table demo:
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.size(Size::Remainder) // for the table
|
.size(Size::remainder()) // for the table
|
||||||
.size(Size::Absolute(10.0)) // for the source code link
|
.size(Size::exact(10.0)) // for the source code link
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
strip.cell_clip(|ui| {
|
strip.cell_clip(|ui| {
|
||||||
self.table_ui(ui);
|
self.table_ui(ui);
|
||||||
|
@ -49,24 +51,25 @@ impl TableDemo {
|
||||||
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
||||||
TableBuilder::new(ui)
|
TableBuilder::new(ui)
|
||||||
.striped(true)
|
.striped(true)
|
||||||
.column(Size::Absolute(120.0))
|
.column(Size::initial(60.0).at_least(40.0))
|
||||||
.column(Size::RemainderMinimum(180.0))
|
.column(Size::remainder().at_least(60.0))
|
||||||
.column(Size::Absolute(100.0))
|
.column(Size::initial(60.0).at_least(40.0))
|
||||||
|
.resizable(self.resizable)
|
||||||
.header(20.0, |mut header| {
|
.header(20.0, |mut header| {
|
||||||
header.col(|ui| {
|
header.col_clip(|ui| {
|
||||||
ui.heading("Left");
|
ui.heading("Left");
|
||||||
});
|
});
|
||||||
header.col(|ui| {
|
header.col_clip(|ui| {
|
||||||
ui.heading("Middle");
|
ui.heading("Middle");
|
||||||
});
|
});
|
||||||
header.col(|ui| {
|
header.col_clip(|ui| {
|
||||||
ui.heading("Right");
|
ui.heading("Right");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.body(|mut body| {
|
.body(|mut body| {
|
||||||
if self.virtual_scroll {
|
if self.virtual_scroll {
|
||||||
body.rows(20.0, 100_000, |index, mut row| {
|
body.rows(20.0, 100_000, |index, mut row| {
|
||||||
row.col(|ui| {
|
row.col_clip(|ui| {
|
||||||
ui.label(index.to_string());
|
ui.label(index.to_string());
|
||||||
});
|
});
|
||||||
row.col_clip(|ui| {
|
row.col_clip(|ui| {
|
||||||
|
@ -75,30 +78,27 @@ impl TableDemo {
|
||||||
.wrap(false),
|
.wrap(false),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col_clip(|ui| {
|
||||||
ui.label(index.to_string());
|
ui.label(index.to_string());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
for i in 0..100 {
|
for i in 0..20 {
|
||||||
let height = match i % 8 {
|
let thick = i % 4 == 0;
|
||||||
0 => 25.0,
|
let height = if thick { 25.0 } else { 15.0 };
|
||||||
4 => 30.0,
|
|
||||||
_ => 20.0,
|
|
||||||
};
|
|
||||||
body.row(height, |mut row| {
|
body.row(height, |mut row| {
|
||||||
row.col(|ui| {
|
row.col_clip(|ui| {
|
||||||
ui.label(i.to_string());
|
ui.label(i.to_string());
|
||||||
});
|
});
|
||||||
row.col_clip(|ui| {
|
row.col_clip(|ui| {
|
||||||
ui.add(
|
ui.style_mut().wrap = Some(false);
|
||||||
egui::Label::new(
|
if thick {
|
||||||
format!("Normal scroll, each row can have a different height. Height: {}", height),
|
ui.heading("Extra thick row");
|
||||||
)
|
} else {
|
||||||
.wrap(false),
|
ui.label("Normal row");
|
||||||
);
|
}
|
||||||
});
|
});
|
||||||
row.col(|ui| {
|
row.col_clip(|ui| {
|
||||||
ui.label(i.to_string());
|
ui.label(i.to_string());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,7 @@ All notable changes to the `egui_extras` integration will be noted in this file.
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
* Added `Strip`, `Table` and `DatePicker` ([#963](https://github.com/emilk/egui/pull/963)).
|
||||||
|
|
||||||
|
|
||||||
## 0.17.0 - 2022-02-22
|
## 0.17.0 - 2022-02-22
|
||||||
|
|
|
@ -56,7 +56,7 @@ impl<'a> DatePickerPopup<'a> {
|
||||||
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
|
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
|
||||||
StripBuilder::new(ui)
|
StripBuilder::new(ui)
|
||||||
.sizes(
|
.sizes(
|
||||||
Size::Absolute(height),
|
Size::exact(height),
|
||||||
match (self.combo_boxes, self.arrows) {
|
match (self.combo_boxes, self.arrows) {
|
||||||
(true, true) => 2,
|
(true, true) => 2,
|
||||||
(true, false) | (false, true) => 1,
|
(true, false) | (false, true) => 1,
|
||||||
|
@ -64,14 +64,14 @@ impl<'a> DatePickerPopup<'a> {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.sizes(
|
.sizes(
|
||||||
Size::Absolute((spacing + height) * (weeks.len() + 1) as f32),
|
Size::exact((spacing + height) * (weeks.len() + 1) as f32),
|
||||||
if self.calendar { 1 } else { 0 },
|
if self.calendar { 1 } else { 0 },
|
||||||
)
|
)
|
||||||
.size(Size::Absolute(height))
|
.size(Size::exact(height))
|
||||||
.vertical(|mut strip| {
|
.vertical(|mut strip| {
|
||||||
if self.combo_boxes {
|
if self.combo_boxes {
|
||||||
strip.strip_clip(|builder| {
|
strip.strip_clip(|builder| {
|
||||||
builder.sizes(Size::Remainder, 3).horizontal(|mut strip| {
|
builder.sizes(Size::remainder(), 3).horizontal(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
ComboBox::from_id_source("date_picker_year")
|
ComboBox::from_id_source("date_picker_year")
|
||||||
.selected_text(popup_state.year.to_string())
|
.selected_text(popup_state.year.to_string())
|
||||||
|
@ -138,7 +138,7 @@ impl<'a> DatePickerPopup<'a> {
|
||||||
|
|
||||||
if self.arrows {
|
if self.arrows {
|
||||||
strip.strip(|builder| {
|
strip.strip(|builder| {
|
||||||
builder.sizes(Size::Remainder, 6).horizontal(|mut strip| {
|
builder.sizes(Size::remainder(), 6).horizontal(|mut strip| {
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
|
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
|
||||||
if ui
|
if ui
|
||||||
|
@ -236,7 +236,7 @@ impl<'a> DatePickerPopup<'a> {
|
||||||
ui.spacing_mut().item_spacing = Vec2::new(1.0, 2.0);
|
ui.spacing_mut().item_spacing = Vec2::new(1.0, 2.0);
|
||||||
TableBuilder::new(ui)
|
TableBuilder::new(ui)
|
||||||
.scroll(false)
|
.scroll(false)
|
||||||
.columns(Size::Remainder, if self.calendar_week { 8 } else { 7 })
|
.columns(Size::remainder(), if self.calendar_week { 8 } else { 7 })
|
||||||
.header(height, |mut header| {
|
.header(height, |mut header| {
|
||||||
if self.calendar_week {
|
if self.calendar_week {
|
||||||
header.col(|ui| {
|
header.col(|ui| {
|
||||||
|
@ -322,7 +322,7 @@ impl<'a> DatePickerPopup<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
strip.strip(|builder| {
|
strip.strip(|builder| {
|
||||||
builder.sizes(Size::Remainder, 3).horizontal(|mut strip| {
|
builder.sizes(Size::remainder(), 3).horizontal(|mut strip| {
|
||||||
strip.empty();
|
strip.empty();
|
||||||
strip.cell(|ui| {
|
strip.cell(|ui| {
|
||||||
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
|
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
|
||||||
|
|
|
@ -99,10 +99,9 @@ impl<'l> StripLayout<'l> {
|
||||||
add_contents: impl FnOnce(&mut Ui),
|
add_contents: impl FnOnce(&mut Ui),
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let rect = self.cell_rect(&width, &height);
|
let rect = self.cell_rect(&width, &height);
|
||||||
self.cell(rect, clip, add_contents);
|
let used_rect = self.cell(rect, clip, add_contents);
|
||||||
self.set_pos(rect);
|
self.set_pos(rect);
|
||||||
|
self.ui.allocate_rect(rect.union(used_rect), Sense::hover())
|
||||||
self.ui.allocate_rect(rect, Sense::click())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_striped(
|
pub(crate) fn add_striped(
|
||||||
|
@ -138,7 +137,7 @@ impl<'l> StripLayout<'l> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cell(&mut self, rect: Rect, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
|
fn cell(&mut self, rect: Rect, clip: bool, add_contents: impl FnOnce(&mut Ui)) -> Rect {
|
||||||
let mut child_ui = self.ui.child_ui(rect, *self.ui.layout());
|
let mut child_ui = self.ui.child_ui(rect, *self.ui.layout());
|
||||||
|
|
||||||
if clip {
|
if clip {
|
||||||
|
@ -149,6 +148,7 @@ impl<'l> StripLayout<'l> {
|
||||||
}
|
}
|
||||||
|
|
||||||
add_contents(&mut child_ui);
|
add_contents(&mut child_ui);
|
||||||
|
child_ui.min_rect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size
|
/// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size
|
||||||
|
|
|
@ -1,26 +1,84 @@
|
||||||
/// Size hint for table column/strip cell
|
/// Size hint for table column/strip cell.
|
||||||
#[derive(Clone, Debug, Copy)]
|
#[derive(Clone, Debug, Copy)]
|
||||||
pub enum Size {
|
pub enum Size {
|
||||||
/// Absolute size in points
|
/// Absolute size in points, with a given range of allowed sizes to resize within.
|
||||||
Absolute(f32),
|
Absolute { initial: f32, range: (f32, f32) },
|
||||||
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
|
/// Relative size relative to all available space.
|
||||||
Relative(f32),
|
Relative { fraction: f32, range: (f32, f32) },
|
||||||
/// [`Size::Relative`] with a minimum size in points
|
/// Multiple remainders each get the same space.
|
||||||
RelativeMinimum {
|
Remainder { range: (f32, f32) },
|
||||||
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
|
}
|
||||||
relative: f32,
|
|
||||||
/// Absolute minimum size in points
|
impl Size {
|
||||||
minimum: f32,
|
/// Exactly this big, with no room for resize.
|
||||||
},
|
pub fn exact(points: f32) -> Self {
|
||||||
/// Multiple remainders each get the same space
|
Self::Absolute {
|
||||||
Remainder,
|
initial: points,
|
||||||
/// [`Size::Remainder`] with a minimum size in points
|
range: (points, points),
|
||||||
RemainderMinimum(f32),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initially this big, but can resize.
|
||||||
|
pub fn initial(points: f32) -> Self {
|
||||||
|
Self::Absolute {
|
||||||
|
initial: points,
|
||||||
|
range: (0.0, f32::INFINITY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`.
|
||||||
|
pub fn relative(fraction: f32) -> Self {
|
||||||
|
egui::egui_assert!(0.0 <= fraction && fraction <= 1.0);
|
||||||
|
Self::Relative {
|
||||||
|
fraction,
|
||||||
|
range: (0.0, f32::INFINITY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiple remainders each get the same space.
|
||||||
|
pub fn remainder() -> Self {
|
||||||
|
Self::Remainder {
|
||||||
|
range: (0.0, f32::INFINITY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Won't shrink below this size (in points).
|
||||||
|
pub fn at_least(mut self, minimum: f32) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
Self::Absolute { range, .. }
|
||||||
|
| Self::Relative { range, .. }
|
||||||
|
| Self::Remainder { range, .. } => {
|
||||||
|
range.0 = minimum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Won't grow above this size (in points).
|
||||||
|
pub fn at_most(mut self, maximum: f32) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
Self::Absolute { range, .. }
|
||||||
|
| Self::Relative { range, .. }
|
||||||
|
| Self::Remainder { range, .. } => {
|
||||||
|
range.1 = maximum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allowed range of movement (in points), if in a resizable [`Table`].
|
||||||
|
pub fn range(self) -> (f32, f32) {
|
||||||
|
match self {
|
||||||
|
Self::Absolute { range, .. }
|
||||||
|
| Self::Relative { range, .. }
|
||||||
|
| Self::Remainder { range, .. } => range,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Sizing {
|
pub struct Sizing {
|
||||||
sizes: Vec<Size>,
|
pub(crate) sizes: Vec<Size>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizing {
|
impl Sizing {
|
||||||
|
@ -32,24 +90,21 @@ impl Sizing {
|
||||||
self.sizes.push(size);
|
self.sizes.push(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_lengths(self, length: f32, spacing: f32) -> Vec<f32> {
|
pub fn to_lengths(&self, length: f32, spacing: f32) -> Vec<f32> {
|
||||||
let mut remainders = 0;
|
let mut remainders = 0;
|
||||||
let sum_non_remainder = self
|
let sum_non_remainder = self
|
||||||
.sizes
|
.sizes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|size| match size {
|
.map(|&size| match size {
|
||||||
Size::Absolute(absolute) => *absolute,
|
Size::Absolute { initial, .. } => initial,
|
||||||
Size::Relative(relative) => {
|
Size::Relative {
|
||||||
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
|
fraction,
|
||||||
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
|
range: (min, max),
|
||||||
length * relative
|
} => {
|
||||||
|
assert!(0.0 <= fraction && fraction <= 1.0);
|
||||||
|
(length * fraction).clamp(min, max)
|
||||||
}
|
}
|
||||||
Size::RelativeMinimum { relative, minimum } => {
|
Size::Remainder { .. } => {
|
||||||
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
|
|
||||||
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
|
|
||||||
minimum.max(length * relative)
|
|
||||||
}
|
|
||||||
Size::Remainder | Size::RemainderMinimum(..) => {
|
|
||||||
remainders += 1;
|
remainders += 1;
|
||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
|
@ -62,10 +117,10 @@ impl Sizing {
|
||||||
} else {
|
} else {
|
||||||
let mut remainder_length = length - sum_non_remainder;
|
let mut remainder_length = length - sum_non_remainder;
|
||||||
let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor();
|
let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor();
|
||||||
self.sizes.iter().for_each(|size| {
|
self.sizes.iter().for_each(|&size| {
|
||||||
if let Size::RemainderMinimum(minimum) = size {
|
if let Size::Remainder { range: (min, _max) } = size {
|
||||||
if *minimum > avg_remainder_length {
|
if avg_remainder_length < min {
|
||||||
remainder_length -= minimum;
|
remainder_length -= min;
|
||||||
remainders -= 1;
|
remainders -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,13 +133,14 @@ impl Sizing {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sizes
|
self.sizes
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|size| match size {
|
.map(|&size| match size {
|
||||||
Size::Absolute(absolute) => absolute,
|
Size::Absolute { initial, .. } => initial,
|
||||||
Size::Relative(relative) => length * relative,
|
Size::Relative {
|
||||||
Size::RelativeMinimum { relative, minimum } => minimum.max(length * relative),
|
fraction,
|
||||||
Size::Remainder => avg_remainder_length,
|
range: (min, max),
|
||||||
Size::RemainderMinimum(minimum) => minimum.max(avg_remainder_length),
|
} => (length * fraction).clamp(min, max),
|
||||||
|
Size::Remainder { range: (min, max) } => avg_remainder_length.clamp(min, max),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -98,23 +154,16 @@ impl From<Vec<Size>> for Sizing {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sizing() {
|
fn test_sizing() {
|
||||||
let sizing: Sizing = vec![Size::RemainderMinimum(20.0), Size::Remainder].into();
|
let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into();
|
||||||
assert_eq!(sizing.clone().into_lengths(50.0, 0.0), vec![25.0, 25.0]);
|
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]);
|
||||||
assert_eq!(sizing.clone().into_lengths(30.0, 0.0), vec![20.0, 10.0]);
|
assert_eq!(sizing.to_lengths(30.0, 0.0), vec![20.0, 10.0]);
|
||||||
assert_eq!(sizing.clone().into_lengths(20.0, 0.0), vec![20.0, 0.0]);
|
assert_eq!(sizing.to_lengths(20.0, 0.0), vec![20.0, 0.0]);
|
||||||
assert_eq!(sizing.clone().into_lengths(10.0, 0.0), vec![20.0, 0.0]);
|
assert_eq!(sizing.to_lengths(10.0, 0.0), vec![20.0, 0.0]);
|
||||||
assert_eq!(sizing.into_lengths(20.0, 10.0), vec![20.0, 0.0]);
|
assert_eq!(sizing.to_lengths(20.0, 10.0), vec![20.0, 0.0]);
|
||||||
|
|
||||||
let sizing: Sizing = vec![
|
let sizing: Sizing = vec![Size::relative(0.5).at_least(10.0), Size::exact(10.0)].into();
|
||||||
Size::RelativeMinimum {
|
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 10.0]);
|
||||||
relative: 0.5,
|
assert_eq!(sizing.to_lengths(30.0, 0.0), vec![15.0, 10.0]);
|
||||||
minimum: 10.0,
|
assert_eq!(sizing.to_lengths(20.0, 0.0), vec![10.0, 10.0]);
|
||||||
},
|
assert_eq!(sizing.to_lengths(10.0, 0.0), vec![10.0, 10.0]);
|
||||||
Size::Absolute(10.0),
|
|
||||||
]
|
|
||||||
.into();
|
|
||||||
assert_eq!(sizing.clone().into_lengths(50.0, 0.0), vec![25.0, 10.0]);
|
|
||||||
assert_eq!(sizing.clone().into_lengths(30.0, 0.0), vec![15.0, 10.0]);
|
|
||||||
assert_eq!(sizing.clone().into_lengths(20.0, 0.0), vec![10.0, 10.0]);
|
|
||||||
assert_eq!(sizing.into_lengths(10.0, 0.0), vec![10.0, 10.0]);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,11 @@ use egui::{Response, Ui};
|
||||||
/// # egui::__run_test_ui(|ui| {
|
/// # egui::__run_test_ui(|ui| {
|
||||||
/// use egui_extras::{StripBuilder, Size};
|
/// use egui_extras::{StripBuilder, Size};
|
||||||
/// StripBuilder::new(ui)
|
/// StripBuilder::new(ui)
|
||||||
/// .size(Size::RemainderMinimum(100.0))
|
/// .size(Size::remainder().at_least(100.0))
|
||||||
/// .size(Size::Absolute(40.0))
|
/// .size(Size::exact(40.0))
|
||||||
/// .vertical(|mut strip| {
|
/// .vertical(|mut strip| {
|
||||||
/// strip.strip(|builder| {
|
/// strip.strip(|builder| {
|
||||||
/// builder.sizes(Size::Remainder, 2).horizontal(|mut strip| {
|
/// builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||||
/// strip.cell(|ui| {
|
/// strip.cell(|ui| {
|
||||||
/// ui.label("Top Left");
|
/// ui.label("Top Left");
|
||||||
/// });
|
/// });
|
||||||
|
@ -72,7 +72,7 @@ impl<'a> StripBuilder<'a> {
|
||||||
where
|
where
|
||||||
F: for<'b> FnOnce(Strip<'a, 'b>),
|
F: for<'b> FnOnce(Strip<'a, 'b>),
|
||||||
{
|
{
|
||||||
let widths = self.sizing.into_lengths(
|
let widths = self.sizing.to_lengths(
|
||||||
self.ui.available_rect_before_wrap().width() - self.ui.spacing().item_spacing.x,
|
self.ui.available_rect_before_wrap().width() - self.ui.spacing().item_spacing.x,
|
||||||
self.ui.spacing().item_spacing.x,
|
self.ui.spacing().item_spacing.x,
|
||||||
);
|
);
|
||||||
|
@ -93,7 +93,7 @@ impl<'a> StripBuilder<'a> {
|
||||||
where
|
where
|
||||||
F: for<'b> FnOnce(Strip<'a, 'b>),
|
F: for<'b> FnOnce(Strip<'a, 'b>),
|
||||||
{
|
{
|
||||||
let heights = self.sizing.into_lengths(
|
let heights = self.sizing.to_lengths(
|
||||||
self.ui.available_rect_before_wrap().height() - self.ui.spacing().item_spacing.y,
|
self.ui.available_rect_before_wrap().height() - self.ui.spacing().item_spacing.y,
|
||||||
self.ui.spacing().item_spacing.y,
|
self.ui.spacing().item_spacing.y,
|
||||||
);
|
);
|
||||||
|
@ -117,6 +117,10 @@ pub struct Strip<'a, 'b> {
|
||||||
|
|
||||||
impl<'a, 'b> Strip<'a, 'b> {
|
impl<'a, 'b> Strip<'a, 'b> {
|
||||||
fn next_cell_size(&mut self) -> (CellSize, CellSize) {
|
fn next_cell_size(&mut self) -> (CellSize, CellSize) {
|
||||||
|
assert!(
|
||||||
|
!self.sizes.is_empty(),
|
||||||
|
"Tried using more strip cells than available."
|
||||||
|
);
|
||||||
let size = self.sizes[0];
|
let size = self.sizes[0];
|
||||||
self.sizes = &self.sizes[1..];
|
self.sizes = &self.sizes[1..];
|
||||||
|
|
||||||
|
@ -128,21 +132,11 @@ impl<'a, 'b> Strip<'a, 'b> {
|
||||||
|
|
||||||
/// Add empty cell
|
/// Add empty cell
|
||||||
pub fn empty(&mut self) {
|
pub fn empty(&mut self) {
|
||||||
assert!(
|
|
||||||
!self.sizes.is_empty(),
|
|
||||||
"Tried using more strip cells than available."
|
|
||||||
);
|
|
||||||
|
|
||||||
let (width, height) = self.next_cell_size();
|
let (width, height) = self.next_cell_size();
|
||||||
self.layout.empty(width, height);
|
self.layout.empty(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cell_impl(&mut self, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
|
fn cell_impl(&mut self, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
|
||||||
assert!(
|
|
||||||
!self.sizes.is_empty(),
|
|
||||||
"Tried using more strip cells than available."
|
|
||||||
);
|
|
||||||
|
|
||||||
let (width, height) = self.next_cell_size();
|
let (width, height) = self.next_cell_size();
|
||||||
self.layout.add(width, height, clip, add_contents);
|
self.layout.add(width, height, clip, add_contents);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use egui::{Response, Ui};
|
use egui::{Response, Ui};
|
||||||
use std::cmp;
|
|
||||||
|
|
||||||
/// Builder for a [`Table`] with (optional) fixed header and scrolling body.
|
/// Builder for a [`Table`] with (optional) fixed header and scrolling body.
|
||||||
///
|
///
|
||||||
|
@ -26,8 +25,8 @@ use std::cmp;
|
||||||
/// # egui::__run_test_ui(|ui| {
|
/// # egui::__run_test_ui(|ui| {
|
||||||
/// use egui_extras::{TableBuilder, Size};
|
/// use egui_extras::{TableBuilder, Size};
|
||||||
/// TableBuilder::new(ui)
|
/// TableBuilder::new(ui)
|
||||||
/// .column(Size::RemainderMinimum(100.0))
|
/// .column(Size::remainder().at_least(100.0))
|
||||||
/// .column(Size::Absolute(40.0))
|
/// .column(Size::exact(40.0))
|
||||||
/// .header(20.0, |mut header| {
|
/// .header(20.0, |mut header| {
|
||||||
/// header.col(|ui| {
|
/// header.col(|ui| {
|
||||||
/// ui.heading("Growing");
|
/// ui.heading("Growing");
|
||||||
|
@ -53,6 +52,7 @@ pub struct TableBuilder<'a> {
|
||||||
sizing: Sizing,
|
sizing: Sizing,
|
||||||
scroll: bool,
|
scroll: bool,
|
||||||
striped: bool,
|
striped: bool,
|
||||||
|
resizable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TableBuilder<'a> {
|
impl<'a> TableBuilder<'a> {
|
||||||
|
@ -64,6 +64,7 @@ impl<'a> TableBuilder<'a> {
|
||||||
sizing,
|
sizing,
|
||||||
scroll: true,
|
scroll: true,
|
||||||
striped: false,
|
striped: false,
|
||||||
|
resizable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +80,17 @@ impl<'a> TableBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Make the columns resizable by dragging.
|
||||||
|
///
|
||||||
|
/// Default is `false`.
|
||||||
|
///
|
||||||
|
/// If you have multiple [`Table`]:s in the same [`Ui`]
|
||||||
|
/// you will need to give them unique id:s with [`Ui::push_id`].
|
||||||
|
pub fn resizable(mut self, resizable: bool) -> Self {
|
||||||
|
self.resizable = resizable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add size hint for column
|
/// Add size hint for column
|
||||||
pub fn column(mut self, width: Size) -> Self {
|
pub fn column(mut self, width: Size) -> Self {
|
||||||
self.sizing.add(width);
|
self.sizing.add(width);
|
||||||
|
@ -106,10 +118,26 @@ impl<'a> TableBuilder<'a> {
|
||||||
/// Create a header row which always stays visible and at the top
|
/// Create a header row which always stays visible and at the top
|
||||||
pub fn header(self, height: f32, header: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
|
pub fn header(self, height: f32, header: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
|
||||||
let available_width = self.available_width();
|
let available_width = self.available_width();
|
||||||
let widths = self
|
|
||||||
.sizing
|
let Self {
|
||||||
.into_lengths(available_width, self.ui.spacing().item_spacing.x);
|
ui,
|
||||||
let ui = self.ui;
|
sizing,
|
||||||
|
scroll,
|
||||||
|
striped,
|
||||||
|
resizable,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
|
||||||
|
let widths = if let Some(resize_id) = resize_id {
|
||||||
|
ui.data().get_persisted(resize_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let widths = widths
|
||||||
|
.unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x));
|
||||||
|
|
||||||
|
let table_top = ui.min_rect().bottom();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut layout = StripLayout::new(ui, CellDirection::Horizontal);
|
let mut layout = StripLayout::new(ui, CellDirection::Horizontal);
|
||||||
header(TableRow {
|
header(TableRow {
|
||||||
|
@ -124,9 +152,12 @@ impl<'a> TableBuilder<'a> {
|
||||||
|
|
||||||
Table {
|
Table {
|
||||||
ui,
|
ui,
|
||||||
|
table_top,
|
||||||
|
resize_id,
|
||||||
|
sizing,
|
||||||
widths,
|
widths,
|
||||||
scroll: self.scroll,
|
scroll,
|
||||||
striped: self.striped,
|
striped,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,15 +167,34 @@ impl<'a> TableBuilder<'a> {
|
||||||
F: for<'b> FnOnce(TableBody<'b>),
|
F: for<'b> FnOnce(TableBody<'b>),
|
||||||
{
|
{
|
||||||
let available_width = self.available_width();
|
let available_width = self.available_width();
|
||||||
let widths = self
|
|
||||||
.sizing
|
let Self {
|
||||||
.into_lengths(available_width, self.ui.spacing().item_spacing.x);
|
ui,
|
||||||
|
sizing,
|
||||||
|
scroll,
|
||||||
|
striped,
|
||||||
|
resizable,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
|
||||||
|
let widths = if let Some(resize_id) = resize_id {
|
||||||
|
ui.data().get_persisted(resize_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let widths = widths
|
||||||
|
.unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x));
|
||||||
|
|
||||||
|
let table_top = ui.min_rect().bottom();
|
||||||
|
|
||||||
Table {
|
Table {
|
||||||
ui: self.ui,
|
ui,
|
||||||
|
table_top,
|
||||||
|
resize_id,
|
||||||
|
sizing,
|
||||||
widths,
|
widths,
|
||||||
scroll: self.scroll,
|
scroll,
|
||||||
striped: self.striped,
|
striped,
|
||||||
}
|
}
|
||||||
.body(body);
|
.body(body);
|
||||||
}
|
}
|
||||||
|
@ -155,6 +205,9 @@ impl<'a> TableBuilder<'a> {
|
||||||
/// Is created by [`TableBuilder`] by either calling [`TableBuilder::body`] or after creating a header row with [`TableBuilder::header`].
|
/// Is created by [`TableBuilder`] by either calling [`TableBuilder::body`] or after creating a header row with [`TableBuilder::header`].
|
||||||
pub struct Table<'a> {
|
pub struct Table<'a> {
|
||||||
ui: &'a mut Ui,
|
ui: &'a mut Ui,
|
||||||
|
table_top: f32,
|
||||||
|
resize_id: Option<egui::Id>,
|
||||||
|
sizing: Sizing,
|
||||||
widths: Vec<f32>,
|
widths: Vec<f32>,
|
||||||
scroll: bool,
|
scroll: bool,
|
||||||
striped: bool,
|
striped: bool,
|
||||||
|
@ -168,26 +221,91 @@ impl<'a> Table<'a> {
|
||||||
{
|
{
|
||||||
let Table {
|
let Table {
|
||||||
ui,
|
ui,
|
||||||
|
table_top,
|
||||||
|
resize_id,
|
||||||
|
sizing,
|
||||||
widths,
|
widths,
|
||||||
scroll,
|
scroll,
|
||||||
striped,
|
striped,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let start_y = ui.available_rect_before_wrap().top();
|
let avail_rect = ui.available_rect_before_wrap();
|
||||||
let end_y = ui.available_rect_before_wrap().bottom();
|
|
||||||
|
|
||||||
egui::ScrollArea::new([false, scroll]).show(ui, move |ui| {
|
let mut new_widths = widths.clone();
|
||||||
let layout = StripLayout::new(ui, CellDirection::Horizontal);
|
|
||||||
|
|
||||||
body(TableBody {
|
egui::ScrollArea::new([false, scroll])
|
||||||
layout,
|
.auto_shrink([true; 2])
|
||||||
widths,
|
.show(ui, move |ui| {
|
||||||
striped,
|
let layout = StripLayout::new(ui, CellDirection::Horizontal);
|
||||||
row_nr: 0,
|
|
||||||
start_y,
|
body(TableBody {
|
||||||
end_y,
|
layout,
|
||||||
|
widths,
|
||||||
|
striped,
|
||||||
|
row_nr: 0,
|
||||||
|
start_y: avail_rect.top(),
|
||||||
|
end_y: avail_rect.bottom(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
let bottom = ui.min_rect().bottom();
|
||||||
|
|
||||||
|
// TODO: fix frame-delay by interacting before laying out (but painting later).
|
||||||
|
if let Some(resize_id) = resize_id {
|
||||||
|
let spacing_x = ui.spacing().item_spacing.x;
|
||||||
|
let mut x = avail_rect.left() - spacing_x * 0.5;
|
||||||
|
for (i, width) in new_widths.iter_mut().enumerate() {
|
||||||
|
x += *width + spacing_x;
|
||||||
|
|
||||||
|
let resize_id = ui.id().with("__panel_resize").with(i);
|
||||||
|
|
||||||
|
let mut p0 = egui::pos2(x, table_top);
|
||||||
|
let mut p1 = egui::pos2(x, bottom);
|
||||||
|
let line_rect = egui::Rect::from_min_max(p0, p1)
|
||||||
|
.expand(ui.style().interaction.resize_grab_radius_side);
|
||||||
|
let mouse_over_resize_line = ui.rect_contains_pointer(line_rect);
|
||||||
|
|
||||||
|
if ui.input().pointer.any_pressed()
|
||||||
|
&& ui.input().pointer.any_down()
|
||||||
|
&& mouse_over_resize_line
|
||||||
|
{
|
||||||
|
ui.memory().set_dragged_id(resize_id);
|
||||||
|
}
|
||||||
|
let is_resizing = ui.memory().is_being_dragged(resize_id);
|
||||||
|
if is_resizing {
|
||||||
|
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
|
||||||
|
let new_width = *width + pointer.x - x;
|
||||||
|
let (min, max) = sizing.sizes[i].range();
|
||||||
|
let new_width = new_width.clamp(min, max);
|
||||||
|
let x = x - *width + new_width;
|
||||||
|
p0.x = x;
|
||||||
|
p1.x = x;
|
||||||
|
|
||||||
|
*width = new_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dragging_something_else =
|
||||||
|
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
|
||||||
|
let resize_hover = mouse_over_resize_line && !dragging_something_else;
|
||||||
|
|
||||||
|
if resize_hover || is_resizing {
|
||||||
|
ui.output().cursor_icon = egui::CursorIcon::ResizeHorizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stroke = if is_resizing {
|
||||||
|
ui.style().visuals.widgets.active.bg_stroke
|
||||||
|
} else if resize_hover {
|
||||||
|
ui.style().visuals.widgets.hovered.bg_stroke
|
||||||
|
} else {
|
||||||
|
// ui.visuals().widgets.inactive.bg_stroke
|
||||||
|
ui.visuals().widgets.noninteractive.bg_stroke
|
||||||
|
};
|
||||||
|
ui.painter().line_segment([p0, p1], stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.data().insert_persisted(resize_id, new_widths);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,7 +344,7 @@ impl<'a> TableBody<'a> {
|
||||||
|
|
||||||
let max_height = self.end_y - self.start_y;
|
let max_height = self.end_y - self.start_y;
|
||||||
let count = (max_height / height).ceil() as usize;
|
let count = (max_height / height).ceil() as usize;
|
||||||
let end = cmp::min(start + count, rows);
|
let end = rows.min(start + count);
|
||||||
|
|
||||||
for idx in start..end {
|
for idx in start..end {
|
||||||
row(
|
row(
|
||||||
|
|
Loading…
Reference in a new issue