Conversation
|
@EArminjon is attempting to deploy a commit to the Widgetbook Team on Vercel. A member of the Team first needs to authorize it. |
|
To view this pull requests documentation preview, visit the following URL: docs.page/widgetbook/widgetbook~1784 Documentation is deployed and generated using docs.page. |
34a337b to
092bf7f
Compare
| this.minScale = 1, | ||
| this.maxScale = 10, |
There was a problem hiding this comment.
We cannot introduce breaking changes to the constructor. By removing initialZoom, when users upgrade, their Widgetbook will stop working. Instead, we should deprecate initialZoom and introduce the new params alongside it. You can have a look at TextScaleAddon where we had a similar change.
| /// | ||
| /// Learn more: https://docs.widgetbook.io/addons/zoom-addon | ||
| class ZoomAddon extends WidgetbookAddon<double> { | ||
| class ZoomAddon extends WidgetbookAddon<bool> { |
There was a problem hiding this comment.
By changing this addon to bool addon, then we lose the concept of "configuring" this addon on Widgetbook Cloud.
Imagine if users are testing are using a ZoomAddonConfig with a value of 2. This means that they expect their screenshot to be zoomed/scaled 2x.
With this change here, the addon becomes less configurable and turns into a "dev" addon (similar to inspector addon), where you turn it on or off only during development to help you build better widgets.
So we have two options here:
- Debate if
ZoomAddonConfigwas of any use in the first place, and deprecate it with this change, as we cannot also introduce breaking changes (double -> bool) there. (Open for discussions) - Instead of editing this addon, we can introduce a new
PanAddonorInteractiveZoomAddonand keep the old addon as-is.
This problem can be avoided if you found a way to make InteractiveViewer to zoom to certain factor from the center. Then we can revert changes to ZoomAddonConfig.
There was a problem hiding this comment.
PR updated to avoid impacting ZoomAddon.
I strongly suggest reconsidering the relevance of ZoomAddon, as it only zooms the center of a use case and provides limited value.
There was a problem hiding this comment.
I agree that we need to reconsider the ZoomAddon. I see that there are no Widgetbook Cloud users that use the ZoomAddonConfig, so we can change the addon to be a "dev addon" as follows, to avoid addons clutter:
- Deprecate
ZoomAddonConfig. - Move
InteractiveZoomAddonback toZoomAddonand update it accordingly without any constructor breaking changes. I would argue that we don't even need themin/maxscales as well. We can hardcode it to value that make sense to simplify the API. - Deprecate
initialZoomfrom theZoomAddonconstructor as well.
8e81b14 to
a3ba1ab
Compare
|
I would like to go deeper inside the interactive viewer addon. Actually, when a dev 'hot reload' then scale and X/Y are reset. import 'dart:async';
import 'package:flutter/material.dart';
import 'package:widgetbook/widgetbook.dart';
/// A [WidgetbookAddon] for zoom/scaling the widget tree.
///
/// The zoom addon allows developers to scale the entire widget tree
/// to test how components behave at different zoom levels.
///
/// Learn more: https://docs.widgetbook.io/addons/zoom-addon
class CustomZoomAddon extends WidgetbookAddon<bool> {
/// Creates a new instance of [ZoomAddon].
CustomZoomAddon({this.minScale = 1, this.maxScale = 30}) : super(name: 'Zoom');
final double minScale;
final double maxScale;
@override
List<Field> get fields => [BooleanField(name: 'enabled')];
@override
bool valueFromQueryGroup(Map<String, String> group) {
return valueOf('enabled', group) as bool;
}
@override
Widget buildUseCase(BuildContext context, Widget child, bool setting) {
if (!setting) return child;
return InteractiveViewerWidget(minScale: minScale, maxScale: maxScale, child: child);
}
}
class InteractiveViewerWidget extends StatefulWidget {
const InteractiveViewerWidget({required this.minScale, required this.maxScale, required this.child, super.key});
final double minScale;
final double maxScale;
final Widget child;
@override
State<InteractiveViewerWidget> createState() => _InteractiveViewerWidgetState();
}
class _InteractiveViewerWidgetState extends State<InteractiveViewerWidget> {
TransformationController? _controller;
Timer? _debounceTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final screenSize = MediaQuery.sizeOf(context);
final state = WidgetbookState.of(context);
final groupMap = FieldCodec.decodeQueryGroup(state.queryParams['zoom']);
/// Parse scale with default of 1
final scale = double.tryParse(groupMap['scale'] ?? '1') ?? 1;
/// Parse x and y with fallback to center
final fallbackX = (screenSize.width / 2) * (1 - scale);
final fallbackY = (screenSize.height / 2) * (1 - scale);
final x = double.tryParse(groupMap['x'] ?? '') ?? fallbackX;
final y = double.tryParse(groupMap['y'] ?? '') ?? fallbackY;
_controller = TransformationController(Matrix4.identity()..scale(scale));
_controller!.value.setTranslationRaw(x, y, 0);
_controller!.addListener(_onTransformChanged);
setState(() {});
});
}
void _onTransformChanged() {
final scale = _controller!.value.getMaxScaleOnAxis().toStringAsFixed(2);
final x = _controller!.value.getTranslation().x.toStringAsFixed(2);
final y = _controller!.value.getTranslation().y.toStringAsFixed(2);
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 100), () {
WidgetbookState.of(context)
..updateQueryField(group: 'zoom', field: 'scale', value: scale)
..updateQueryField(group: 'zoom', field: 'x', value: x)
..updateQueryField(group: 'zoom', field: 'y', value: y);
});
}
@override
void dispose() {
_controller?.removeListener(_onTransformChanged);
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return InteractiveViewer(
transformationController: _controller,
minScale: widget.minScale,
maxScale: widget.maxScale,
child: widget.child,
);
}
} |
yousinix
left a comment
There was a problem hiding this comment.
Hello @EArminjon! 👋
Apology for the very late response, we have been busy and on multiple vacations.
First of all, we should settle on ZoomAddon vs InteractiveViewerAddon. After doing research of the ZoomAddon usage, I see it's not so useful in its current state, so we can go back to your initial proposal of making this as the new ZoomAddon. This comes with:
- Removing any breaking changes to the constructor.
- Deprecating the
ZoomAddonConfigas it's no longer needed.
I also like the idea of making this addon persist it's state across hot reloads. It can be beneficial. Here are some ideas that can make the code more concrete:
- We can change the addon from using
boolto useZoomSettingas a type, this will help restore the state from the group easier. - Making the
InteractiveViewerWidgetunaware of Widgetbook by passing any callbacks or data from outside. - Instead of calling
updateQueryField3 times, we can callupdateQueryParamonce, to reduce rebuilds.
Here's a high level code that you can use as a starter. Feel free to do any renames or type changes based on your needs.
@internal
class ZoomSetting {
const ZoomSetting({
required this.enabled,
required this.scale,
required this.x,
required this.y,
});
final bool enabled;
final double scale;
final double x;
final double y;
}
class ZoomAddon extends WidgetbookAddon<ZoomSetting> {
/// Creates a new instance of [ZoomAddon].
ZoomAddon() : super(name: 'Zoom');
@override
List<Field> get fields => [BooleanField(name: 'enabled')];
@override
ZoomSetting valueFromQueryGroup(Map<String, String> group) {
final enabled = valueOf('enabled', group) as bool;
return ZoomSetting(
enabled: enabled,
scale: double.tryParse(group['scale'] ?? '') ?? 1,
x: double.tryParse(group['x'] ?? '') ?? 0,
y: double.tryParse(group['y'] ?? '') ?? 0,
);
}
@override
Widget buildUseCase(BuildContext context, Widget child, ZoomSetting setting) {
if (!setting.enabled) return child;
return _RestorableInteractiveWidget(
minScale: 1, // Fixed for now
maxScale: 30, // Fixed for now
translation: Point(setting.x, setting.y),
scale: setting.scale,
child: child,
onTransformChanged: (translation, scale) {
final newGroup = {
'enabled': setting.enabled.toString(),
'scale': scale.toStringAsFixed(2),
'x': translation.x.toStringAsFixed(2),
'y': translation.y.toStringAsFixed(2),
};
final encodedNewGroup = FieldCodec.encodeQueryGroup(newGroup);
WidgetbookState.of(context).updateQueryParam('zoom', encodedNewGroup);
},
);
}
}
// TODO: Extract this widget to separate file
class _RestorableInteractiveWidget extends StatefulWidget {
const _RestorableInteractiveWidget({
required this.minScale,
required this.maxScale,
required this.scale,
required this.translation,
required this.onTransformChanged,
required this.child,
});
final double minScale;
final double maxScale;
final double scale;
final Point translation;
final void Function(Point translation, double scale) onTransformChanged;
final Widget child;
@override
State<_RestorableInteractiveWidget> createState() =>
_RestorableInteractiveWidgetState();
}
class _RestorableInteractiveWidgetState
extends State<_RestorableInteractiveWidget> {
TransformationController? _controller;
Timer? _debounceTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// TODO:
// Instead of using `WidgetbookState.of(context)` here,
// use widget.translation and widget.scale to set the initial transformation
setState(() {});
});
}
void _onTransformChanged() {
final x = _controller!.value.getTranslation().x;
final y = _controller!.value.getTranslation().y;
final scale = _controller!.value.getMaxScaleOnAxis();
_debounceTimer?.cancel();
_debounceTimer = Timer(
const Duration(milliseconds: 100),
() => widget.onTransformChanged(Point(x, y), scale),
);
}
@override
void dispose() {
_controller?.removeListener(_onTransformChanged);
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return InteractiveViewer(
transformationController: _controller,
minScale: widget.minScale,
maxScale: widget.maxScale,
child: widget.child,
);
}
}a3ba1ab to
ce90171
Compare
Hi, I've reworked the InteractiveViewerAddon, tell me if something is wrong. I didn't touched ZoomAddon, i let you keep one if you prefer. |
199b025 to
f3bcfc8
Compare
f3bcfc8 to
8b74ce0
Compare
|
(Unit test and CI test fixed) |
This PR rework ZoomAddon to use an InteractiveViewer instead of a Transform.scale() to let user zoom where he want instead of only the middle.
List of issues which are fixed by the PR
Screenshots
Enregistrement.de.l.ecran.2025-12-17.a.09.48.35.mov
Checklist
///).If you need help, consider asking for advice on Discord.