diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fa88477..22b1083 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.3' + - '1.6' - '1' - 'nightly' os: diff --git a/src/GridLayoutBase.jl b/src/GridLayoutBase.jl index 1bb5597..32f5eb3 100644 --- a/src/GridLayoutBase.jl +++ b/src/GridLayoutBase.jl @@ -12,11 +12,8 @@ const DEFAULT_COLGAP_GETTER = Ref{Function}(() -> DEFAULT_COLGAP[]) include("types.jl") include("gridlayout.jl") -include("layout_engine.jl") include("layoutobservables.jl") -include("gridapi.jl") include("helpers.jl") -include("geometry_integration.jl") include("gridlayoutspec.jl") export GridLayout, GridPosition diff --git a/src/geometry_integration.jl b/src/geometry_integration.jl deleted file mode 100644 index 0581c16..0000000 --- a/src/geometry_integration.jl +++ /dev/null @@ -1,57 +0,0 @@ -left(rect::Rect{2}) = minimum(rect)[1] -right(rect::Rect{2}) = maximum(rect)[1] -bottom(rect::Rect{2}) = minimum(rect)[2] -top(rect::Rect{2}) = maximum(rect)[2] - -width(rect::Rect{2}) = right(rect) - left(rect) -height(rect::Rect{2}) = top(rect) - bottom(rect) - - -function BBox(left::Number, right::Number, bottom::Number, top::Number) - mini = (left, bottom) - maxi = (right, top) - return Rect2f(mini, maxi .- mini) -end - - -function RowCols(ncols::Int, nrows::Int) - return RowCols( - zeros(Float32, ncols), - zeros(Float32, ncols), - zeros(Float32, nrows), - zeros(Float32, nrows) - ) -end - -Base.getindex(rowcols::RowCols, ::Left) = rowcols.lefts -Base.getindex(rowcols::RowCols, ::Right) = rowcols.rights -Base.getindex(rowcols::RowCols, ::Top) = rowcols.tops -Base.getindex(rowcols::RowCols, ::Bottom) = rowcols.bottoms - -""" - eachside(f) -Calls f over all sides (Left, Right, Top, Bottom), and creates a BBox from the result of f(side) -""" -function eachside(f) -    return BBox(f(Left()), f(Right()), f(Bottom()), f(Top())) -end - -""" -mapsides( - f, first::Union{Rect{2}, RowCols}, rest::Union{Rect{2}, RowCols}... - )::Rect2f -Maps f over all sides of the rectangle like arguments. -e.g. -``` -mapsides(BBox(left, right, bottom, top)) do side::Side, side_val::Number - return ... -end::Rect2f -``` -""" -function mapsides( - f, first::Union{Rect{2}, RowCols}, rest::Union{Rect{2}, RowCols}... - ) - return eachside() do side - f(side, getindex.((first, rest...), (side,))...) - end -end diff --git a/src/gridapi.jl b/src/gridapi.jl deleted file mode 100644 index 3c4bdfc..0000000 --- a/src/gridapi.jl +++ /dev/null @@ -1,69 +0,0 @@ -""" - grid!(content::Vararg{Pair}; kwargs...) - -Creates a GridLayout with all pairs contained in `content`. Each pair consists -of an iterable with row and column spans, and a content object. Each content -object is then placed in the GridLayout at the span from its pair. - -Example: - -grid!( - [1, 1] => obj1, - [1, 2] => obj2, - [2, :] => obj3, -) -""" -function grid!(content::Vararg{Pair}; kwargs...) - g = GridLayout(; kwargs...) - for ((rows, cols), element) in content - g[rows, cols] = element - end - g -end - -""" - hbox!(content::Vararg; kwargs...) - -Creates a single-row GridLayout with all elements contained in `content` placed -from left to right. -""" -function hbox!(content::Vararg; kwargs...) - ncols = length(content) - g = GridLayout(1, ncols; kwargs...) - for (i, element) in enumerate(content) - g[1, i] = element - end - g -end - -""" - vbox!(content::Vararg; kwargs...) - -Creates a single-column GridLayout with all elements contained in `content` placed -from top to bottom. -""" -function vbox!(content::Vararg; kwargs...) - nrows = length(content) - g = GridLayout(nrows, 1; kwargs...) - for (i, element) in enumerate(content) - g[i, 1] = element - end - g -end - - - -""" - grid!(content::AbstractMatrix; kwargs...) - -Creates a GridLayout filled with matrix-like content. The size of the grid will -be the size of the matrix. -""" -function grid!(content::AbstractMatrix; kwargs...) - nrows, ncols = size(content) - g = GridLayout(nrows, ncols; kwargs...) - for i in 1:nrows, j in 1:ncols - g[i, j] = content[i, j] - end - g -end diff --git a/src/gridlayout.jl b/src/gridlayout.jl index 4ca753a..93d8ab4 100644 --- a/src/gridlayout.jl +++ b/src/gridlayout.jl @@ -1501,4 +1501,252 @@ function top_parent_grid(g::GridLayout) else g end -end \ No newline at end of file +end + +""" + side_indices(c::GridContent)::RowCols{Int} + +Indices of the rows / cols for each side +""" +function side_indices(c::GridContent) + return RowCols( + c.span.cols.start, + c.span.cols.stop, + c.span.rows.start, + c.span.rows.stop, + ) +end + +# These functions tell whether an object in a grid touches the left, top, etc. border +# of the grid. This means that it is relevant for the grid's own protrusion on that side. +ismostin(gc::GridContent, grid, ::Left) = gc.span.cols.start == 1 +ismostin(gc::GridContent, grid, ::Right) = gc.span.cols.stop == grid.ncols +ismostin(gc::GridContent, grid, ::Bottom) = gc.span.rows.stop == grid.nrows +ismostin(gc::GridContent, grid, ::Top) = gc.span.rows.start == 1 + + +function protrusion(x::T, side::Side) where T + protrusions = protrusionsobservable(x) + if side isa Left + protrusions[].left + elseif side isa Right + protrusions[].right + elseif side isa Bottom + protrusions[].bottom + elseif side isa Top + protrusions[].top + else + error("Can't get a protrusion value for side $(typeof(side)), only + Left, Right, Bottom, or Top.") + end +end + +function protrusion(gc::GridContent, side::Side) + prot = + if gc.side isa Inner + protrusion(gc.content, side) + # elseif gc.side isa Outer; BBox(l - pl, r + pr, b - pb, t + pt) + elseif gc.side isa Union{Left, Right} + if side isa typeof(gc.side) + determinedirsize(gc.content, Col(), gc.side) + else + 0.0 + end + elseif gc.side isa Union{Top, Bottom} + if side isa typeof(gc.side) + determinedirsize(gc.content, Row(), gc.side) + else + 0.0 + end + elseif gc.side isa TopLeft + if side isa Top + determinedirsize(gc.content, Row(), gc.side) + elseif side isa Left + determinedirsize(gc.content, Col(), gc.side) + else + 0.0 + end + elseif gc.side isa TopRight + if side isa Top + determinedirsize(gc.content, Row(), gc.side) + elseif side isa Right + determinedirsize(gc.content, Col(), gc.side) + else + 0.0 + end + elseif gc.side isa BottomLeft + if side isa Bottom + determinedirsize(gc.content, Row(), gc.side) + elseif side isa Left + determinedirsize(gc.content, Col(), gc.side) + else + 0.0 + end + elseif gc.side isa BottomRight + if side isa Bottom + determinedirsize(gc.content, Row(), gc.side) + elseif side isa Right + determinedirsize(gc.content, Col(), gc.side) + else + 0.0 + end + else + error("Invalid side $(gc.side)") + end + ifnothing(prot, 0.0) +end + +getside(m::Mixed, ::Left) = m.sides.left +getside(m::Mixed, ::Right) = m.sides.right +getside(m::Mixed, ::Top) = m.sides.top +getside(m::Mixed, ::Bottom) = m.sides.bottom + +function inside_protrusion(gl::GridLayout, side::Side) + prot = 0.0 + for elem in gl.content + if ismostin(elem, gl, side) + # take the max protrusion of all elements that are sticking + # out at this side + prot = max(protrusion(elem, side), prot) + end + end + return prot +end + +function protrusion(gl::GridLayout, side::Side) + # when we align with the outside there is by definition no protrusion + if gl.alignmode[] isa Outside + return 0.0 + elseif gl.alignmode[] isa Inside + inside_protrusion(gl, side) + elseif gl.alignmode[] isa Mixed + si = getside(gl.alignmode[], side) + if isnothing(si) + inside_protrusion(gl, side) + elseif si isa Protrusion + si.p + else + # Outside alignment + 0.0 + end + else + error("Unknown AlignMode of type $(typeof(gl.alignmode[]))") + end +end + +function bbox_for_solving_from_side(maxgrid::RowCols, bbox_cell::Rect2f, idx_rect::RowCols, side::Side) + pl = maxgrid.lefts[idx_rect.lefts] + pr = maxgrid.rights[idx_rect.rights] + pt = maxgrid.tops[idx_rect.tops] + pb = maxgrid.bottoms[idx_rect.bottoms] + + l = left(bbox_cell) + r = right(bbox_cell) + b = bottom(bbox_cell) + t = top(bbox_cell) + + if side isa Inner + bbox_cell + elseif side isa Outer + BBox(l - pl, r + pr, b - pb, t + pt) + elseif side isa Left + BBox(l - pl, l, b, t) + elseif side isa Top + BBox(l, r, t, t + pt) + elseif side isa Right + BBox(r, r + pr, b, t) + elseif side isa Bottom + BBox(l, r, b - pb, b) + elseif side isa TopLeft + BBox(l - pl, l, t, t + pt) + elseif side isa TopRight + BBox(r, r + pr, t, t + pt) + elseif side isa BottomRight + BBox(r, r + pr, b - pb, b) + elseif side isa BottomLeft + BBox(l - pl, l, b - pb, b) + else + error("Invalid side $side") + end +end + +startside(c::Col) = Left() +stopside(c::Col) = Right() +startside(r::Row) = Top() +stopside(r::Row) = Bottom() + + +getspan(gc::GridContent, dir::Col) = gc.span.cols +getspan(gc::GridContent, dir::Row) = gc.span.rows + + + +""" +Determine the size of a protrusion layout along a dimension. This size is dependent +on the `Side` at which the layout is placed in its parent grid. An `Inside` side +means that the protrusion layout reports its width but not its protrusions. `Left` +means that the layout reports only its full width but not its height, because +an element placed in the left protrusion loses its ability to influence height. +""" +function determinedirsize(content, gdir::GridDir, side::Side) + reportedsize = reportedsizeobservable(content) + if gdir isa Row + if side isa Union{Inner, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight} + # TODO: is reportedsize the correct thing to return? or plus protrusions depending on the side + ifnothing(reportedsize[][2], nothing) + elseif side isa Union{Left, Right} + nothing + else + error("$side not implemented") + end + else + if side isa Union{Inner, Left, Right, TopLeft, TopRight, BottomLeft, BottomRight} + ifnothing(reportedsize[][1], nothing) + elseif side isa Union{Top, Bottom} + nothing + else + error("$side not implemented") + end + end +end + +function to_ranges(g::GridLayout, rows::Indexables, cols::Indexables) + if rows isa Int + rows = rows:rows + elseif rows isa Colon + rows = 1:g.nrows + end + if cols isa Int + cols = cols:cols + elseif cols isa Colon + cols = 1:g.ncols + end + rows, cols +end + +function adjust_rows_cols!(g::GridLayout, rows, cols; update = true) + rows, cols = to_ranges(g, rows, cols) + + if rows.start < 1 + n = 1 - rows.start + prependrows!(g, n, update = update) + # adjust rows for the newly prepended ones + rows = rows .+ n + end + if rows.stop > g.nrows + n = rows.stop - g.nrows + appendrows!(g, n, update = update) + end + if cols.start < 1 + n = 1 - cols.start + prependcols!(g, n, update = update) + # adjust cols for the newly prepended ones + cols = cols .+ n + end + if cols.stop > g.ncols + n = cols.stop - g.ncols + appendcols!(g, n, update = update) + end + + rows, cols +end diff --git a/src/helpers.jl b/src/helpers.jl index 0b96b61..d85e0b1 100644 --- a/src/helpers.jl +++ b/src/helpers.jl @@ -35,3 +35,133 @@ function zcumsum(v::AbstractVector{T}) where T vpad = [[zero(T)]; v] # inference-friendly cumsum!(vpad, vpad) end + + +""" + grid!(content::Vararg{Pair}; kwargs...) + +Creates a GridLayout with all pairs contained in `content`. Each pair consists +of an iterable with row and column spans, and a content object. Each content +object is then placed in the GridLayout at the span from its pair. + +Example: + +grid!( + [1, 1] => obj1, + [1, 2] => obj2, + [2, :] => obj3, +) +""" +function grid!(content::Vararg{Pair}; kwargs...) + g = GridLayout(; kwargs...) + for ((rows, cols), element) in content + g[rows, cols] = element + end + g +end + +""" + hbox!(content::Vararg; kwargs...) + +Creates a single-row GridLayout with all elements contained in `content` placed +from left to right. +""" +function hbox!(content::Vararg; kwargs...) + ncols = length(content) + g = GridLayout(1, ncols; kwargs...) + for (i, element) in enumerate(content) + g[1, i] = element + end + g +end + +""" + vbox!(content::Vararg; kwargs...) + +Creates a single-column GridLayout with all elements contained in `content` placed +from top to bottom. +""" +function vbox!(content::Vararg; kwargs...) + nrows = length(content) + g = GridLayout(nrows, 1; kwargs...) + for (i, element) in enumerate(content) + g[i, 1] = element + end + g +end + + + +""" + grid!(content::AbstractMatrix; kwargs...) + +Creates a GridLayout filled with matrix-like content. The size of the grid will +be the size of the matrix. +""" +function grid!(content::AbstractMatrix; kwargs...) + nrows, ncols = size(content) + g = GridLayout(nrows, ncols; kwargs...) + for i in 1:nrows, j in 1:ncols + g[i, j] = content[i, j] + end + g +end + + +left(rect::Rect{2}) = minimum(rect)[1] +right(rect::Rect{2}) = maximum(rect)[1] +bottom(rect::Rect{2}) = minimum(rect)[2] +top(rect::Rect{2}) = maximum(rect)[2] + +width(rect::Rect{2}) = right(rect) - left(rect) +height(rect::Rect{2}) = top(rect) - bottom(rect) + + +function BBox(left::Number, right::Number, bottom::Number, top::Number) + mini = (left, bottom) + maxi = (right, top) + return Rect2f(mini, maxi .- mini) +end + + +function RowCols(ncols::Int, nrows::Int) + return RowCols( + zeros(Float32, ncols), + zeros(Float32, ncols), + zeros(Float32, nrows), + zeros(Float32, nrows) + ) +end + +Base.getindex(rowcols::RowCols, ::Left) = rowcols.lefts +Base.getindex(rowcols::RowCols, ::Right) = rowcols.rights +Base.getindex(rowcols::RowCols, ::Top) = rowcols.tops +Base.getindex(rowcols::RowCols, ::Bottom) = rowcols.bottoms + +""" + eachside(f) +Calls f over all sides (Left, Right, Top, Bottom), and creates a BBox from the result of f(side) +""" +function eachside(f) +    return BBox(f(Left()), f(Right()), f(Bottom()), f(Top())) +end + +""" +mapsides( + f, first::Union{Rect{2}, RowCols}, rest::Union{Rect{2}, RowCols}... + )::Rect2f +Maps f over all sides of the rectangle like arguments. +e.g. +``` +mapsides(BBox(left, right, bottom, top)) do side::Side, side_val::Number + return ... +end::Rect2f +``` +""" +function mapsides( + f, first::Union{Rect{2}, RowCols}, rest::Union{Rect{2}, RowCols}... + ) + return eachside() do side + f(side, getindex.((first, rest...), (side,))...) + end +end diff --git a/src/layout_engine.jl b/src/layout_engine.jl deleted file mode 100644 index 241a02d..0000000 --- a/src/layout_engine.jl +++ /dev/null @@ -1,247 +0,0 @@ -""" - side_indices(c::GridContent)::RowCols{Int} - -Indices of the rows / cols for each side -""" -function side_indices(c::GridContent) - return RowCols( - c.span.cols.start, - c.span.cols.stop, - c.span.rows.start, - c.span.rows.stop, - ) -end - -# These functions tell whether an object in a grid touches the left, top, etc. border -# of the grid. This means that it is relevant for the grid's own protrusion on that side. -ismostin(gc::GridContent, grid, ::Left) = gc.span.cols.start == 1 -ismostin(gc::GridContent, grid, ::Right) = gc.span.cols.stop == grid.ncols -ismostin(gc::GridContent, grid, ::Bottom) = gc.span.rows.stop == grid.nrows -ismostin(gc::GridContent, grid, ::Top) = gc.span.rows.start == 1 - - -function protrusion(x::T, side::Side) where T - protrusions = protrusionsobservable(x) - if side isa Left - protrusions[].left - elseif side isa Right - protrusions[].right - elseif side isa Bottom - protrusions[].bottom - elseif side isa Top - protrusions[].top - else - error("Can't get a protrusion value for side $(typeof(side)), only - Left, Right, Bottom, or Top.") - end -end - -function protrusion(gc::GridContent, side::Side) - prot = - if gc.side isa Inner - protrusion(gc.content, side) - # elseif gc.side isa Outer; BBox(l - pl, r + pr, b - pb, t + pt) - elseif gc.side isa Union{Left, Right} - if side isa typeof(gc.side) - determinedirsize(gc.content, Col(), gc.side) - else - 0.0 - end - elseif gc.side isa Union{Top, Bottom} - if side isa typeof(gc.side) - determinedirsize(gc.content, Row(), gc.side) - else - 0.0 - end - elseif gc.side isa TopLeft - if side isa Top - determinedirsize(gc.content, Row(), gc.side) - elseif side isa Left - determinedirsize(gc.content, Col(), gc.side) - else - 0.0 - end - elseif gc.side isa TopRight - if side isa Top - determinedirsize(gc.content, Row(), gc.side) - elseif side isa Right - determinedirsize(gc.content, Col(), gc.side) - else - 0.0 - end - elseif gc.side isa BottomLeft - if side isa Bottom - determinedirsize(gc.content, Row(), gc.side) - elseif side isa Left - determinedirsize(gc.content, Col(), gc.side) - else - 0.0 - end - elseif gc.side isa BottomRight - if side isa Bottom - determinedirsize(gc.content, Row(), gc.side) - elseif side isa Right - determinedirsize(gc.content, Col(), gc.side) - else - 0.0 - end - else - error("Invalid side $(gc.side)") - end - ifnothing(prot, 0.0) -end - -getside(m::Mixed, ::Left) = m.sides.left -getside(m::Mixed, ::Right) = m.sides.right -getside(m::Mixed, ::Top) = m.sides.top -getside(m::Mixed, ::Bottom) = m.sides.bottom - -function inside_protrusion(gl::GridLayout, side::Side) - prot = 0.0 - for elem in gl.content - if ismostin(elem, gl, side) - # take the max protrusion of all elements that are sticking - # out at this side - prot = max(protrusion(elem, side), prot) - end - end - return prot -end - -function protrusion(gl::GridLayout, side::Side) - # when we align with the outside there is by definition no protrusion - if gl.alignmode[] isa Outside - return 0.0 - elseif gl.alignmode[] isa Inside - inside_protrusion(gl, side) - elseif gl.alignmode[] isa Mixed - si = getside(gl.alignmode[], side) - if isnothing(si) - inside_protrusion(gl, side) - elseif si isa Protrusion - si.p - else - # Outside alignment - 0.0 - end - else - error("Unknown AlignMode of type $(typeof(gl.alignmode[]))") - end -end - -function bbox_for_solving_from_side(maxgrid::RowCols, bbox_cell::Rect2f, idx_rect::RowCols, side::Side) - pl = maxgrid.lefts[idx_rect.lefts] - pr = maxgrid.rights[idx_rect.rights] - pt = maxgrid.tops[idx_rect.tops] - pb = maxgrid.bottoms[idx_rect.bottoms] - - l = left(bbox_cell) - r = right(bbox_cell) - b = bottom(bbox_cell) - t = top(bbox_cell) - - if side isa Inner - bbox_cell - elseif side isa Outer - BBox(l - pl, r + pr, b - pb, t + pt) - elseif side isa Left - BBox(l - pl, l, b, t) - elseif side isa Top - BBox(l, r, t, t + pt) - elseif side isa Right - BBox(r, r + pr, b, t) - elseif side isa Bottom - BBox(l, r, b - pb, b) - elseif side isa TopLeft - BBox(l - pl, l, t, t + pt) - elseif side isa TopRight - BBox(r, r + pr, t, t + pt) - elseif side isa BottomRight - BBox(r, r + pr, b - pb, b) - elseif side isa BottomLeft - BBox(l - pl, l, b - pb, b) - else - error("Invalid side $side") - end -end - -startside(c::Col) = Left() -stopside(c::Col) = Right() -startside(r::Row) = Top() -stopside(r::Row) = Bottom() - - -getspan(gc::GridContent, dir::Col) = gc.span.cols -getspan(gc::GridContent, dir::Row) = gc.span.rows - - - -""" -Determine the size of a protrusion layout along a dimension. This size is dependent -on the `Side` at which the layout is placed in its parent grid. An `Inside` side -means that the protrusion layout reports its width but not its protrusions. `Left` -means that the layout reports only its full width but not its height, because -an element placed in the left protrusion loses its ability to influence height. -""" -function determinedirsize(content, gdir::GridDir, side::Side) - reportedsize = reportedsizeobservable(content) - if gdir isa Row - if side isa Union{Inner, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight} - # TODO: is reportedsize the correct thing to return? or plus protrusions depending on the side - ifnothing(reportedsize[][2], nothing) - elseif side isa Union{Left, Right} - nothing - else - error("$side not implemented") - end - else - if side isa Union{Inner, Left, Right, TopLeft, TopRight, BottomLeft, BottomRight} - ifnothing(reportedsize[][1], nothing) - elseif side isa Union{Top, Bottom} - nothing - else - error("$side not implemented") - end - end -end - -function to_ranges(g::GridLayout, rows::Indexables, cols::Indexables) - if rows isa Int - rows = rows:rows - elseif rows isa Colon - rows = 1:g.nrows - end - if cols isa Int - cols = cols:cols - elseif cols isa Colon - cols = 1:g.ncols - end - rows, cols -end - -function adjust_rows_cols!(g::GridLayout, rows, cols; update = true) - rows, cols = to_ranges(g, rows, cols) - - if rows.start < 1 - n = 1 - rows.start - prependrows!(g, n, update = update) - # adjust rows for the newly prepended ones - rows = rows .+ n - end - if rows.stop > g.nrows - n = rows.stop - g.nrows - appendrows!(g, n, update = update) - end - if cols.start < 1 - n = 1 - cols.start - prependcols!(g, n, update = update) - # adjust cols for the newly prepended ones - cols = cols .+ n - end - if cols.stop > g.ncols - n = cols.stop - g.ncols - appendcols!(g, n, update = update) - end - - rows, cols -end