139 lines
4.5 KiB
Swift
139 lines
4.5 KiB
Swift
//
|
||
// AspectContainer.swift
|
||
//
|
||
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
|
||
//
|
||
// This program is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// This program is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU General Public License
|
||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
//
|
||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
//
|
||
|
||
import Adwaita
|
||
import CGtkWidgets
|
||
|
||
/// A single‑child container that preserves a fixed aspect ratio.
|
||
///
|
||
/// Unlike GTK's built‑in `GtkAspectFrame`, this container overrides
|
||
/// `measure()` to report the correct height (`width × ratio`) during
|
||
/// the layout pass. This makes it work correctly inside `ScrollView`,
|
||
/// `AdwCarousel`, and other flexible layouts where the parent needs
|
||
/// to know the child's preferred size.
|
||
///
|
||
/// ```swift
|
||
/// AspectContainer(aspectRatio: 16.0 / 9.0) {
|
||
/// .child {
|
||
/// Picture()
|
||
/// .data(imageData)
|
||
/// .halign(.fill)
|
||
/// .hexpand()
|
||
/// }
|
||
/// .maxWidth(800)
|
||
/// }
|
||
/// ```
|
||
///
|
||
/// Ported 1:1 from the Gelata project's `AspectRatioContainer` Rust widget.
|
||
public struct AspectContainer: Widget {
|
||
|
||
#if exposeGeneratedAppearUpdateFunctions
|
||
public var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
||
public var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
||
#else
|
||
var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
||
var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
||
#endif
|
||
|
||
/// The child widget.
|
||
var child: Body?
|
||
/// The desired width‑to‑height ratio.
|
||
var aspectRatio: Float
|
||
/// An optional maximum width in pixels (0 = no limit).
|
||
var maxWidth: Int?
|
||
|
||
/// Create an aspect‑ratio container.
|
||
/// - Parameter aspectRatio: Desired `width / height` ratio.
|
||
public init(aspectRatio: Float) {
|
||
self.aspectRatio = aspectRatio
|
||
}
|
||
|
||
// MARK: - Widget
|
||
|
||
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage
|
||
where Data: ViewRenderData {
|
||
let storage = ViewStorage(aspect_container_new(aspectRatio)?.opaque())
|
||
for function in appearFunctions {
|
||
function(storage, data)
|
||
}
|
||
if let childStorage = child?.storage(data: data, type: type) {
|
||
storage.content["child"] = [childStorage]
|
||
gtk_widget_set_parent(
|
||
childStorage.opaquePointer?.cast(),
|
||
storage.opaquePointer?.cast())
|
||
}
|
||
return storage
|
||
}
|
||
|
||
public func update<Data>(
|
||
_ storage: ViewStorage,
|
||
data: WidgetData,
|
||
updateProperties: Bool,
|
||
type: Data.Type
|
||
) where Data: ViewRenderData {
|
||
storage.modify { widget in
|
||
if let childStorage = storage.content["child"]?.first {
|
||
child?.updateStorage(
|
||
childStorage,
|
||
data: data,
|
||
updateProperties: updateProperties,
|
||
type: type
|
||
)
|
||
}
|
||
if let maxWidth,
|
||
updateProperties,
|
||
(storage.previousState as? Self)?.maxWidth != maxWidth
|
||
{
|
||
aspect_container_set_max_width(widget, maxWidth.cInt)
|
||
}
|
||
if updateProperties,
|
||
(storage.previousState as? Self)?.aspectRatio != aspectRatio
|
||
{
|
||
aspect_container_set_aspect_ratio(widget, aspectRatio)
|
||
}
|
||
}
|
||
for function in updateFunctions {
|
||
function(storage, data, updateProperties)
|
||
}
|
||
if updateProperties {
|
||
storage.previousState = self
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - Modifiers
|
||
|
||
extension AspectContainer {
|
||
|
||
/// Set the child widget.
|
||
public func child(@ViewBuilder _ child: () -> Body) -> Self {
|
||
modify { $0.child = child() }
|
||
}
|
||
|
||
/// Set an optional maximum width in pixels.
|
||
/// When set to a positive value, the container's width is clamped
|
||
/// before computing the height from the aspect ratio.
|
||
public func maxWidth(_ maxWidth: Int?) -> Self {
|
||
modify { $0.maxWidth = maxWidth }
|
||
}
|
||
|
||
}
|