Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created July 9, 2025 18:56
Show Gist options
  • Select an option

  • Save slightfoot/2f1fb7aa3fb7c78dfc1f06ad94ad1764 to your computer and use it in GitHub Desktop.

Select an option

Save slightfoot/2f1fb7aa3fb7c78dfc1f06ad94ad1764 to your computer and use it in GitHub Desktop.

Revisions

  1. slightfoot created this gist Jul 9, 2025.
    277 changes: 277 additions & 0 deletions humpday_2025-07-09_1.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,277 @@
    // MIT License
    //
    // Copyright (c) 2025 Simon Lightfoot
    //
    // Permission is hereby granted, free of charge, to any person obtaining a copy
    // of this software and associated documentation files (the "Software"), to deal
    // in the Software without restriction, including without limitation the rights
    // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    // copies of the Software, and to permit persons to whom the Software is
    // furnished to do so, subject to the following conditions:
    //
    // The above copyright notice and this permission notice shall be included in all
    // copies or substantial portions of the Software.
    //
    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    // SOFTWARE.
    //

    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';

    /// Idea: https://x.com/aloisdeniel/status/1942685270102409666
    void main() {
    runApp(const App());
    }

    class App extends StatefulWidget {
    const App({super.key});

    @override
    State<App> createState() => _AppState();
    }

    class _AppState extends State<App> with SingleTickerProviderStateMixin {
    late StackCanvasController _controller;

    @override
    void initState() {
    super.initState();
    _controller = StackCanvasController();
    }

    @override
    void dispose() {
    _controller.dispose();
    super.dispose();
    }

    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    debugShowCheckedModeBanner: false,
    home: Material(
    child: DefaultTextStyle.merge(
    style: TextStyle(
    fontSize: 20.0,
    fontWeight: FontWeight.w500,
    ),
    child: StackCanvas(
    controller: _controller,
    children: [
    StackItem(
    rect: Rect.fromLTWH(100, -20, 200, 150),
    builder: (BuildContext context) => DemoItem(
    color: Colors.red,
    label: 'Child 1',
    ),
    ),
    StackItem(
    rect: Rect.fromLTWH(-50, 100, 200, 150),
    builder: (BuildContext context) => DemoItem(
    color: Colors.blue,
    label: 'Child 2',
    ),
    ),
    StackItem(
    rect: Rect.fromLTWH(200, 250, 200, 150),
    builder: (BuildContext context) => DemoItem(
    color: Colors.green,
    label: 'Child 3',
    ),
    ),
    StackItem(
    rect: Rect.fromLTWH(500, 25, 200, 150),
    builder: (BuildContext context) => DemoItem(
    color: Colors.teal,
    label: 'Child 4',
    ),
    ),
    ],
    ),
    ),
    ),
    );
    }
    }

    class DemoItem extends StatelessWidget {
    const DemoItem({
    super.key,
    required this.color,
    required this.label,
    });

    final Color color;
    final String label;

    @override
    Widget build(BuildContext context) {
    return DecoratedBox(
    decoration: BoxDecoration(
    color: color,
    borderRadius: BorderRadius.circular(16.0),
    ),
    child: Center(child: Text(label)),
    );
    }
    }

    class StackItem extends StatelessWidget {
    const StackItem({
    super.key,
    required this.rect,
    required this.builder,
    });

    final Rect rect;
    final WidgetBuilder builder;

    @override
    Widget build(BuildContext context) {
    return Positioned.fromRect(
    rect: rect,
    child: Builder(builder: builder),
    );
    }
    }

    class StackCanvasController extends ChangeNotifier {
    StackCanvasController({
    Offset initialPosition = Offset.zero,
    }) : _origin = initialPosition;

    Offset _origin;

    Offset get origin => _origin;

    set origin(Offset value) {
    if (_origin != value) {
    _origin = value;
    notifyListeners();
    }
    }
    }

    class StackCanvas extends StatelessWidget {
    const StackCanvas({
    super.key,
    required this.controller,
    required this.children,
    });

    final StackCanvasController controller;
    final List<StackItem> children;

    @override
    Widget build(BuildContext context) {
    return GestureDetector(
    behavior: HitTestBehavior.opaque,
    onPanUpdate: (details) {
    controller.origin -= details.delta;
    },
    child: StackCanvasLayout(
    controller: controller,
    children: children,
    ),
    );
    }
    }

    class StackCanvasLayout extends MultiChildRenderObjectWidget {
    const StackCanvasLayout({
    super.key,
    required this.controller,
    required List<StackItem> super.children,
    });

    final StackCanvasController controller;

    @override
    RenderObject createRenderObject(BuildContext context) {
    return RenderStackCanvas(controller: controller);
    }

    @override
    void updateRenderObject(BuildContext context, covariant RenderStackCanvas renderObject) {
    renderObject.controller = controller;
    }
    }

    class RenderStackCanvas extends RenderBox
    with
    ContainerRenderObjectMixin<RenderBox, StackParentData>,
    RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
    RenderStackCanvas({
    required StackCanvasController controller,
    }) : _controller = controller;

    StackCanvasController _controller;

    StackCanvasController get controller => _controller;

    set controller(StackCanvasController value) {
    if (_controller != value) {
    if (attached) {
    _controller.removeListener(_onOriginChanged);
    value.addListener(_onOriginChanged);
    }
    _controller = value;
    _onOriginChanged();
    }
    }

    @override
    void attach(PipelineOwner owner) {
    super.attach(owner);
    controller.addListener(_onOriginChanged);
    }

    @override
    void detach() {
    controller.removeListener(_onOriginChanged);
    super.detach();
    }

    void _onOriginChanged() {
    markNeedsPaint();
    }

    @override
    void setupParentData(RenderBox child) {
    if (child.parentData is! StackParentData) {
    child.parentData = StackParentData();
    }
    }

    @override
    void performLayout() {
    final children = getChildrenAsList();
    for (final child in children) {
    final parentData = child.parentData as StackParentData;
    final childConstraints = BoxConstraints.tightFor(
    width: parentData.width!,
    height: parentData.height!,
    );
    child.layout(childConstraints);
    parentData.offset = Offset(parentData.left!, parentData.top!);
    }
    size = constraints.biggest;
    }

    @override
    bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    return defaultHitTestChildren(result, position: position);
    }

    @override
    void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset - _controller.origin);
    }
    }