Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/images/eraser-area.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/images/eraser-stroke.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions lib/components/canvas/_stroke.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ class Stroke {
}
}

List<PointVector> get pointVectors => List.unmodifiable(points);

void replacePoints(List<PointVector> newPoints) {
points
..clear()
..addAll(newPoints);
markPolygonNeedsUpdating();
}

void popFirstPoint() {
points.removeAt(0);
markPolygonNeedsUpdating();
Expand Down
289 changes: 289 additions & 0 deletions lib/components/toolbar/eraser_modal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:saber/data/extensions/axis_extensions.dart';
import 'package:saber/data/prefs.dart';
import 'package:saber/data/tools/eraser.dart';
import 'package:saber/i18n/strings.g.dart';

class EraserModal extends StatefulWidget {
const EraserModal({super.key, required this.getTool});

final Eraser Function() getTool;

@override
State<EraserModal> createState() => _EraserModalState();
}

class _EraserModalState extends State<EraserModal> {
@override
Widget build(BuildContext context) {
final eraser = widget.getTool();
final axis = stows.editorToolbarAlignment.value.axis.opposite;

return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Flex(
direction: axis,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_EraserSizePicker(
eraser: eraser,
axis: axis,
onDrag: (percent) {
setState(() {
percent = percent.clamp(0.0, 1.0);
final stepsFromMin =
(percent * eraser.sizeStepsBetweenMinAndMax).round();
final newSize = eraser.sizeMin + stepsFromMin * eraser.sizeStep;
eraser.size = newSize;
});
},
),
SizedBox(
width: axis == Axis.horizontal ? 8 : 0,
height: axis == Axis.vertical ? 8 : 0,
),
Flex(
direction: axis,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_EraserModeButton(
selected: !eraser.areaEraser,
tooltip: t.editor.eraserOptions.strokeEraser,
onPressed: () {
setState(() {
eraser.areaEraser = false;
});
},
icon: SvgPicture.asset(
'assets/images/eraser-stroke.svg',
width: 32,
height: 32,
theme: SvgTheme(
currentColor: !eraser.areaEraser
? ColorScheme.of(context).secondary
: ColorScheme.of(context).onSurface,
),
),
),
SizedBox(
width: axis == Axis.horizontal ? 8 : 0,
height: axis == Axis.vertical ? 8 : 0,
),
_EraserModeButton(
selected: eraser.areaEraser,
tooltip: t.editor.eraserOptions.areaEraser,
onPressed: () {
setState(() {
eraser.areaEraser = true;
});
},
icon: SvgPicture.asset(
'assets/images/eraser-area.svg',
width: 32,
height: 32,
theme: SvgTheme(
currentColor: eraser.areaEraser
? ColorScheme.of(context).secondary
: ColorScheme.of(context).onSurface,
),
),
),
],
),
],
),
);
}
}

class _EraserSizePicker extends StatelessWidget {
const _EraserSizePicker({
required this.axis,
required this.eraser,
required this.onDrag,
});

final Axis axis;
final Eraser eraser;
final ValueChanged<double> onDrag;

@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
return Flex(
direction: axis,
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
Text(
t.editor.penOptions.size,
style: TextStyle(
color: colorScheme.onSurface.withValues(alpha: 0.8),
fontSize: 10,
height: 1,
),
),
Text(eraser.size.round().toString()),
],
),
const SizedBox(width: 8),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: _EraserSizeSlider(axis: axis, eraser: eraser, onDrag: onDrag),
),
],
);
}
}

class _EraserSizeSlider extends StatelessWidget {
const _EraserSizeSlider({
required this.axis,
required this.eraser,
required this.onDrag,
});

final Axis axis;
final Eraser eraser;
final ValueChanged<double> onDrag;

@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
return GestureDetector(
// Map drag gestures to a normalized 0..1 percent value used by the
// parent. For horizontal layout we use the local dx; for vertical we
// use local dy. The fixed divisor (150) approximates the slider pixel
// range used by the CustomPaint size.
onHorizontalDragStart: axis == Axis.horizontal
? (details) => onDrag(details.localPosition.dx / 150)
: null,
onHorizontalDragUpdate: axis == Axis.horizontal
? (details) => onDrag(details.localPosition.dx / 150)
: null,
onVerticalDragStart: axis == Axis.vertical
? (details) => onDrag(details.localPosition.dy / 150)
: null,
onVerticalDragUpdate: axis == Axis.vertical
? (details) => onDrag(details.localPosition.dy / 150)
: null,
child: RotatedBox(
quarterTurns: axis == Axis.horizontal ? 0 : 1,
child: CustomPaint(
size: const Size(150, 25),
painter: _EraserSizeSliderPainter(
axis: axis,
minSize: eraser.sizeMin,
maxSize: eraser.sizeMax,
currentSize: eraser.size,
trackColor: colorScheme.onSurface.withValues(alpha: 0.2),
thumbColor: colorScheme.primary,
),
),
),
);
}
}

class _EraserModeButton extends StatelessWidget {
const _EraserModeButton({
required this.selected,
required this.icon,
required this.tooltip,
required this.onPressed,
});

final bool selected;
final Widget icon;
final String tooltip;
final VoidCallback onPressed;

@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
final iconColor = selected ? colorScheme.secondary : colorScheme.onSurface;

return IconButton(
onPressed: onPressed,
style: TextButton.styleFrom(
foregroundColor: iconColor,
backgroundColor: selected
? colorScheme.secondary.withAlpha((0.1 * 255).round())
: Colors.transparent,
shape: const CircleBorder(),
),
tooltip: tooltip,
icon: icon,
);
}
}

class _EraserSizeSliderPainter extends CustomPainter {
_EraserSizeSliderPainter({
required this.axis,
required this.minSize,
required this.maxSize,
required this.currentSize,
required this.trackColor,
required this.thumbColor,
});

final Axis axis;
final double minSize;
final double maxSize;
final double currentSize;
final Color trackColor;
final Color thumbColor;

@override
void paint(Canvas canvas, Size size) {
final leftHeight = size.height * minSize / maxSize;
final topLeft = Offset(0, (size.height - leftHeight) / 2);
final bottomLeft = Offset(0, topLeft.dy + leftHeight);
final topRight = Offset(size.width, 0);
final bottomRight = Offset(size.width, size.height);

canvas.drawPath(
Path()
..moveTo(topLeft.dx, topLeft.dy)
..lineTo(bottomLeft.dx, bottomLeft.dy)
..lineTo(bottomRight.dx, bottomRight.dy)
..lineTo(topRight.dx, topRight.dy)
..close(),
Paint()
..color = trackColor
..style = PaintingStyle.fill,
);

final ratio = (currentSize - minSize) / (maxSize - minSize);
final thumbHeight = size.height * ratio;
final thumbRight = size.width * ratio;
final thumbTop = (size.height - thumbHeight) / 2;
canvas.drawPath(
Path()
..moveTo(topLeft.dx, topLeft.dy)
..lineTo(bottomLeft.dx, bottomLeft.dy)
..lineTo(thumbRight, thumbTop + thumbHeight)
..lineTo(thumbRight, thumbTop)
..close(),
Paint()
..color = thumbColor
..style = PaintingStyle.fill,
);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate is! _EraserSizeSliderPainter ||
oldDelegate.axis != axis ||
oldDelegate.minSize != minSize ||
oldDelegate.maxSize != maxSize ||
oldDelegate.currentSize != currentSize ||
oldDelegate.trackColor != trackColor ||
oldDelegate.thumbColor != thumbColor;
}
}
20 changes: 17 additions & 3 deletions lib/components/toolbar/toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:saber/components/theming/adaptive_icon.dart';
import 'package:saber/components/theming/dynamic_material_app.dart';
import 'package:saber/components/theming/uni_icon.dart';
import 'package:saber/components/toolbar/color_bar.dart';
import 'package:saber/components/toolbar/eraser_modal.dart';
import 'package:saber/components/toolbar/export_bar.dart';
import 'package:saber/components/toolbar/pen_modal.dart';
import 'package:saber/components/toolbar/selection_bar.dart';
Expand Down Expand Up @@ -153,7 +154,7 @@ class _ToolbarState extends State<Toolbar> {

void toggleEraser() {
toolOptionsType.value = .hide;
widget.setTool(Eraser()); // this toggles eraser
widget.setTool(Eraser.currentEraser); // this toggles eraser
}

void toggleColorOptions() {
Expand Down Expand Up @@ -243,6 +244,7 @@ class _ToolbarState extends State<Toolbar> {
getTool: () => Pencil.currentPencil,
setTool: widget.setTool,
),
.eraser => EraserModal(getTool: () => Eraser.currentEraser),
.select => SelectionBar(
duplicateSelection: widget.duplicateSelection,
deleteSelection: widget.deleteSelection,
Expand Down Expand Up @@ -452,7 +454,19 @@ class _ToolbarState extends State<Toolbar> {
tooltip: t.editor.toolbar.toggleEraser,
selected: widget.currentTool is Eraser,
enabled: !widget.readOnly,
onPressed: toggleEraser,
onPressed: () {
// eraser modal
if (widget.currentTool is Eraser) {
if (toolOptionsType.value == .eraser) {
toolOptionsType.value = .hide;
} else {
toolOptionsType.value = .eraser;
}
} else {
toolOptionsType.value = .hide;
widget.setTool(Eraser.currentEraser);
}
},
padding: buttonPadding,
child: const FaIcon(FontAwesomeIcons.eraser, size: 16),
),
Expand Down Expand Up @@ -580,4 +594,4 @@ class _ToolbarState extends State<Toolbar> {
}
}

enum ToolOptions { hide, pen, highlighter, pencil, select }
enum ToolOptions { hide, pen, highlighter, pencil, eraser, select }
Loading