feat: 布局优化

This commit is contained in:
chaoszhu
2026-08-15 09:57:12 +08:00
parent 7cb9f441a3
commit 6c1cc6fcc6
5 changed files with 95 additions and 47 deletions
@@ -27,46 +27,8 @@ class _AgentComposer extends ConsumerWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
minLines: 1,
maxLines: 5,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
hintText: l.tr('agent.inputHint'),
filled: true,
fillColor: context.colors.chip,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
AgentUiTokens.radiusMedium,
),
borderSide: BorderSide.none,
),
suffixIcon: state.conversation.running
? IconButton(
tooltip: l.tr('agent.stop'),
onPressed: ref
.read(agentControllerProvider.notifier)
.stop,
icon: Icon(
Icons.stop_circle,
color: context.colors.danger,
),
)
: ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) => IconButton(
tooltip: l.tr('agent.send'),
onPressed: state.canSendDraft(value.text)
? onSend
: null,
icon: const Icon(Icons.arrow_upward_rounded),
),
),
),
),
const SizedBox(height: 8),
Row(
key: const Key('agent-composer-options'),
children: [
Expanded(
child: _ComposerPickerButton(
@@ -105,6 +67,46 @@ class _AgentComposer extends ConsumerWidget {
),
],
),
const SizedBox(height: 8),
TextField(
key: const Key('agent-composer-input'),
controller: controller,
minLines: 1,
maxLines: 5,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
hintText: l.tr('agent.inputHint'),
filled: true,
fillColor: context.colors.chip,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
AgentUiTokens.radiusMedium,
),
borderSide: BorderSide.none,
),
suffixIcon: state.conversation.running
? IconButton(
tooltip: l.tr('agent.stop'),
onPressed: ref
.read(agentControllerProvider.notifier)
.stop,
icon: Icon(
Icons.stop_circle,
color: context.colors.danger,
),
)
: ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) => IconButton(
tooltip: l.tr('agent.send'),
onPressed: state.canSendDraft(value.text)
? onSend
: null,
icon: const Icon(Icons.arrow_upward_rounded),
),
),
),
),
],
),
),
@@ -91,27 +91,36 @@ class _AgentGlobalOverlayState extends ConsumerState<AgentGlobalOverlay> {
final bottomInset = media.viewInsets.bottom > 0
? media.viewInsets.bottom + AgentUiTokens.edgeGap
: media.padding.bottom + 84;
final maxX = constraints.maxWidth - size - AgentUiTokens.edgeGap;
const minX = AgentUiTokens.entryHorizontalInset;
final maxX =
(constraints.maxWidth -
size -
AgentUiTokens.entryHorizontalInset)
.clamp(minX, double.infinity)
.toDouble();
final maxY = (constraints.maxHeight - size - bottomInset)
.clamp(topInset, double.infinity)
.toDouble();
final range = (maxY - topInset)
.clamp(1.0, double.infinity)
.toDouble();
_lastX =
(_dragX ?? (_side == 'left' ? AgentUiTokens.edgeGap : maxX))
.clamp(AgentUiTokens.edgeGap, maxX)
.toDouble();
_lastX = (_dragX ?? (_side == 'left' ? minX : maxX))
.clamp(minX, maxX)
.toDouble();
_lastY = (_dragY ?? topInset + range * _yFraction)
.clamp(topInset, maxY)
.toDouble();
return Stack(
children: [
Positioned(
AnimatedPositioned(
left: _lastX,
top: _lastY,
width: size,
height: size,
duration: _dragX == null
? AgentUiTokens.entrySnapDuration
: Duration.zero,
curve: Curves.easeOutCubic,
child: FocusableActionDetector(
onShowFocusHighlight: (value) {
if (mounted) setState(() => _entryFocused = value);
@@ -148,7 +157,7 @@ class _AgentGlobalOverlayState extends ConsumerState<AgentGlobalOverlay> {
onPanUpdate: (details) {
setState(() {
_dragX = (_dragX! + details.delta.dx)
.clamp(AgentUiTokens.edgeGap, maxX)
.clamp(minX, maxX)
.toDouble();
_dragY = (_dragY! + details.delta.dy)
.clamp(topInset, maxY)
@@ -1,6 +1,8 @@
abstract final class AgentUiTokens {
static const compactBreakpoint = 720.0;
static const entrySize = 56.0;
static const entryHorizontalInset = 0.0;
static const entrySnapDuration = Duration(milliseconds: 320);
static const edgeGap = 12.0;
static const wideWidth = 480.0;
static const wideHeight = 720.0;
@@ -182,11 +182,42 @@ void main() {
await tester.pump();
final entry = find.byKey(const Key('agent-global-entry'));
final logicalWidth =
tester.view.physicalSize.width / tester.view.devicePixelRatio;
expect(tester.getTopRight(entry).dx, closeTo(logicalWidth, 0.01));
await tester.drag(entry, const Offset(-700, -120));
await tester.pumpAndSettle();
expect(prefs.agentEntrySide, 'left');
expect(prefs.agentEntryY, inInclusiveRange(0.05, 0.95));
expect(tester.getTopLeft(entry).dx, lessThan(20));
expect(tester.getTopLeft(entry).dx, closeTo(0, 0.01));
});
testWidgets('animates to the nearest edge after release', (tester) async {
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = const Size(390, 844);
addTearDown(tester.view.resetDevicePixelRatio);
addTearDown(tester.view.resetPhysicalSize);
final prefs = await storage();
final observer = AgentNavigationObserver();
addTearDown(observer.dispose);
await tester.pumpWidget(app(storage: prefs, observer: observer));
await tester.pump();
final entry = find.byKey(const Key('agent-global-entry'));
await tester.drag(entry, const Offset(-200, 0));
await tester.pump();
final releaseX = tester.getTopLeft(entry).dx;
expect(releaseX, greaterThan(0));
await tester.pump(const Duration(milliseconds: 80));
final transitioningX = tester.getTopLeft(entry).dx;
expect(transitioningX, greaterThan(0));
expect(transitioningX, lessThan(releaseX));
await tester.pumpAndSettle();
expect(tester.getTopLeft(entry).dx, closeTo(0, 0.01));
});
}