fix marker rendering & modernize riding widgets for dark mode - 2026-04-11

This commit is contained in:
Hamza-Ayed
2026-04-11 01:14:09 +03:00
parent 3f03f25142
commit 454276d1e0
88 changed files with 50376 additions and 23310 deletions

View File

@@ -16,8 +16,12 @@ import '../../../controller/functions/tts.dart';
import '../../../controller/home/decode_polyline_isolate.dart';
import '../../../main.dart';
import '../../../print.dart';
import 'dart:ui';
class NavigationController extends GetxController {
import '../../../services/offline_map_service.dart';
class NavigationController extends GetxController
with GetSingleTickerProviderStateMixin {
// ==========================================================================
// ── Tunables ──────────────────────────────────────────────────────────────
// ==========================================================================
@@ -57,11 +61,17 @@ class NavigationController extends GetxController {
/// Updated every tick via angle-aware lerp to eliminate snap/jitter.
double _smoothedHeading = 0.0;
// Animation for smooth tracking
AnimationController? _animController;
LatLng? _oldLoc;
LatLng? _targetLoc;
double currentSpeed = 0.0; // km/h
double totalDistance = 0.0; // metres accumulated this session
// MapLibre objects
Symbol? carSymbol;
Symbol? originSymbol;
Symbol? destinationSymbol;
Line? remainingRouteLine;
Line? traveledRouteLine;
@@ -155,14 +165,31 @@ class NavigationController extends GetxController {
@override
void onInit() {
super.onInit();
_animController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
_animController!.addListener(() {
if (_oldLoc != null && _targetLoc != null && _mapReady) {
final t = _animController!.value;
final lat = lerpDouble(_oldLoc!.latitude, _targetLoc!.latitude, t)!;
final lng = lerpDouble(_oldLoc!.longitude, _targetLoc!.longitude, t)!;
myLocation = LatLng(lat, lng);
if (isStyleLoaded) {
_updateCarMarker();
if (_fullRouteCoordinates.isNotEmpty && _cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading,
zoom: _targetZoom,
tilt: _targetTilt);
}
}
}
});
_initialize();
}
Future<void> _initialize() async {
await _getCurrentLocationAndStartUpdates();
if (!Get.isRegistered<TextToSpeechController>()) {
Get.put(TextToSpeechController());
}
}
@override
@@ -171,6 +198,7 @@ class NavigationController extends GetxController {
_recordTimer?.cancel();
_uploadBatchTimer?.cancel();
_debounce?.cancel();
_animController?.dispose();
mapController?.dispose();
placeDestinationController.dispose();
@@ -294,7 +322,10 @@ class NavigationController extends GetxController {
}
_lastDistanceLocation = newLoc;
myLocation = newLoc;
_oldLoc = myLocation ?? newLoc;
_targetLoc = newLoc;
_animController?.forward(from: 0.0);
_lastProcessedLocation = newLoc;
heading = position.heading;
@@ -306,23 +337,22 @@ class NavigationController extends GetxController {
currentSpeed = position.speed * 3.6;
if (isStyleLoaded) _updateCarMarker();
// Initial visual update if map is fresh
if (isStyleLoaded && myLocation == null) _updateCarMarker();
if (_fullRouteCoordinates.isNotEmpty) {
if (_cameraLockedToUser) {
animateCameraToPosition(myLocation!,
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
}
_updateTraveledPolylineSmart(myLocation!);
_checkNavigationStep(myLocation!);
_updateTraveledPolylineSmart(newLoc);
_checkNavigationStep(newLoc);
_recomputeETA();
// ── Off-route auto-recalculate ─────────────────────────────────────
_checkOffRoute(myLocation!);
_checkOffRoute(newLoc);
}
update();
} catch (_) {}
} catch (e) {
Log.print("Error occurred: $e");
}
}
// ==========================================================================
@@ -498,14 +528,14 @@ class NavigationController extends GetxController {
geometry: myLocation,
iconImage: 'car_icon',
iconSize: 1.0,
iconRotate: _smoothedHeading, // ← use smoothed heading
iconRotate: _smoothedHeading,
));
} else {
mapController!.updateSymbol(
carSymbol!,
SymbolOptions(
geometry: myLocation,
iconRotate: _smoothedHeading, // ← use smoothed heading
iconRotate: _smoothedHeading,
),
);
}
@@ -532,6 +562,50 @@ class NavigationController extends GetxController {
);
}
/// Safe wrapper for animateCamera Bounds to prevent native std::domain_error crash on iOS.
Future<void> _safeAnimateCameraBounds(LatLngBounds? bounds,
{double left = 60,
double top = 60,
double right = 60,
double bottom = 60}) async {
if (bounds == null || mapController == null) return;
try {
// Ensure the coordinates are valid (at least a small span)
final latSpan =
(bounds.northeast.latitude - bounds.southwest.latitude).abs();
final lngSpan =
(bounds.northeast.longitude - bounds.southwest.longitude).abs();
if (latSpan < 0.0001 && lngSpan < 0.0001) {
Log.print(
'⚠️ _safeAnimateCameraBounds: Point-sized bounds, zooming to center.');
mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 16));
return;
}
// Small delay for view stabilization
await Future.delayed(const Duration(milliseconds: 200));
await mapController?.animateCamera(
CameraUpdate.newLatLngBounds(
bounds,
left: left,
top: top,
right: right,
bottom: bottom,
),
);
} catch (e) {
Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED in Nav: $e');
try {
await mapController
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
} catch (_) {}
}
}
void onUserPanned() {
_cameraLockedToUser = false;
update();
@@ -625,38 +699,90 @@ class NavigationController extends GetxController {
// ==========================================================================
Future<void> getRoute(LatLng origin, LatLng destination) async {
// ── Routing Decision: Normal Points -> SaaS, Multi-Stop -> OSRM ──
// Note: NavigationController usually handles the active trip (normal points).
final Map<String, String> queryParams = {
'fromLat': origin.latitude.toString(),
'fromLng': origin.longitude.toString(),
'toLat': destination.latitude.toString(),
'toLng': destination.longitude.toString(),
};
final saasUri =
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
// Fallback OSRM URL
final coords = "${origin.longitude},${origin.latitude};"
"${destination.longitude},${destination.latitude}";
final url =
final osrmUrl =
"$_routeApiBaseUrl/$coords?steps=true&overview=full&geometries=polyline";
try {
final response = await http.get(Uri.parse(url));
// 1. Try SaaS first
http.Response response = await http.get(saasUri, headers: {
'x-api-key': 'intaleq_secret_2026',
});
bool useSaaS = response.statusCode == 200;
if (!useSaaS) {
Log.print("⚠️ SaaS Route failed. Falling back to OSRM...");
response = await http.get(Uri.parse(osrmUrl));
}
if (response.statusCode != 200) {
mySnackbarWarning('تعذر الاتصال بخدمة التوجيه.');
return;
}
final data = jsonDecode(response.body);
if (data['code'] != 'Ok' || (data['routes'] as List).isEmpty) {
final bool isSaaS = useSaaS;
// ── 2. Data Extraction Logic ──────────────────────────────────
String pointsString = "";
dynamic mainRoute;
if (isSaaS) {
pointsString = data['points']?.toString() ?? "";
mainRoute = data; // SaaS structure is top-level
} else {
if (data['code'] != 'Ok' || (data['routes'] as List).isEmpty) {
mySnackbarWarning('لم يتم العثور على مسار.');
return;
}
mainRoute = data['routes'][0];
pointsString = mainRoute['geometry']?.toString() ?? "";
}
if (pointsString.isEmpty) {
mySnackbarWarning('لم يتم العثور على مسار.');
return;
}
final route = data['routes'][0];
_fullRouteCoordinates = await compute<String, List<LatLng>>(
decodePolylineIsolate, route['geometry'].toString());
decodePolylineIsolate, pointsString);
_lastTraveledIndexInFullRoute = 0;
if (isStyleLoaded) _updatePolylinesSets([], _fullRouteCoordinates);
final legs = route['legs'] as List;
if (legs.isNotEmpty) {
routeSteps = List<Map<String, dynamic>>.from(legs[0]['steps'] as List);
// ── Offline Cache: Ensure destination area is stored in memory/disk ───
if (_fullRouteCoordinates.isNotEmpty) {
OfflineMapService.instance
.downloadRegion(_fullRouteCoordinates.last, radiusKm: 2.0);
}
// Handle legs/steps & totals
final legs = mainRoute['legs'] as List?;
if (legs != null && legs.isNotEmpty) {
routeSteps = List<Map<String, dynamic>>.from(legs[0]['steps'] as List);
_routeTotalDistanceM = (legs[0]['distance'] as num).toDouble();
_routeTotalDurationS = (legs[0]['duration'] as num).toDouble();
} else {
// Fallback for SaaS which might have top-level distance/duration
routeSteps = [];
_routeTotalDistanceM = (mainRoute['distance'] as num).toDouble();
_routeTotalDurationS = (mainRoute['duration'] as num).toDouble();
}
if (_routeTotalDistanceM > 0) {
totalDistanceRemaining = _routeTotalDistanceM > 1000
? "${(_routeTotalDistanceM / 1000).toStringAsFixed(1)} كم"
: "${_routeTotalDistanceM.toStringAsFixed(0)} م";
@@ -665,8 +791,6 @@ class NavigationController extends GetxController {
estimatedTimeRemaining = minutes > 60
? "${(minutes / 60).floor()} س ${minutes % 60} د"
: "$minutes د";
} else {
routeSteps = [];
}
for (final step in routeSteps) {
@@ -689,20 +813,18 @@ class NavigationController extends GetxController {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
// ── 5. Camera Update (Safe) ───────────────────────────────────
if (_fullRouteCoordinates.length >= 2) {
final bounds = _boundsFromLatLngList(_fullRouteCoordinates);
final bounds =
data['bbox'] != null && (data['bbox'] as List).length == 4
? LatLngBounds(
southwest: LatLng(data['bbox'][1], data['bbox'][0]),
northeast: LatLng(data['bbox'][3], data['bbox'][2]),
)
: _boundsFromLatLngList(_fullRouteCoordinates);
final latDiff =
(bounds.northeast.latitude - bounds.southwest.latitude).abs();
final lngDiff =
(bounds.northeast.longitude - bounds.southwest.longitude).abs();
if (latDiff > 0.0001 || lngDiff > 0.0001) {
mapController?.animateCamera(CameraUpdate.newLatLngBounds(bounds,
bottom: 220, top: 150, left: 50, right: 50));
} else {
animateCameraToPosition(_fullRouteCoordinates.first, zoom: 15.0);
}
await _safeAnimateCameraBounds(bounds,
bottom: 220, top: 150, left: 50, right: 50);
}
update();
@@ -741,6 +863,7 @@ class NavigationController extends GetxController {
await clearRoute(isNewRoute: true);
if (isStyleLoaded && mapController != null) {
// Destination Marker (B)
destinationSymbol = await mapController!.addSymbol(SymbolOptions(
geometry: destination,
iconImage: 'dest_icon',
@@ -748,6 +871,15 @@ class NavigationController extends GetxController {
textField: infoWindowTitle,
textOffset: const Offset(0, 2),
));
// Start Marker (A)
if (myLocation != null) {
originSymbol = await mapController!.addSymbol(SymbolOptions(
geometry: myLocation,
iconImage: 'start_icon',
iconSize: 1.0,
));
}
}
if (myLocation != null) await getRoute(myLocation!, destination);
@@ -777,6 +909,10 @@ class NavigationController extends GetxController {
await mapController!.removeSymbol(destinationSymbol!);
destinationSymbol = null;
}
if (originSymbol != null && mapController != null) {
await mapController!.removeSymbol(originSymbol!);
originSymbol = null;
}
if (remainingRouteLine != null && mapController != null) {
await mapController!.removeLine(remainingRouteLine!);
remainingRouteLine = null;
@@ -807,8 +943,11 @@ class NavigationController extends GetxController {
Future<void> _loadCustomIcons() async {
if (mapController == null) return;
final carBytes = await rootBundle.load('assets/images/car.png');
final startBytes = await rootBundle.load('assets/images/A.png');
final destBytes = await rootBundle.load('assets/images/b.png');
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
await mapController!.addImage('start_icon', startBytes.buffer.asUint8List());
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
}

View File

@@ -7,12 +7,22 @@ import 'package:maplibre_gl/maplibre_gl.dart';
import 'navigation_controller.dart';
// ─── Brand colours ───────────────────────────────────────────────────────────
const Color _kBlue = Color(0xFF1A73E8);
const Color _kBlueDark = Color(0xFF0D47A1);
const Color _kSurface = Color(0xFFFFFFFF);
const Color _kText = Color(0xFF1C1C1E);
const Color _kSubtext = Color(0xFF6B7280);
const Color _kGreen = Color(0xFF34A853);
// ─── Theme-aware Brand colours ──────────────────────────────────────────────
Color get _kBlue => const Color(0xFF1A73E8);
Color get _kBlueDark => const Color(0xFF0D47A1);
Color get _kSurface =>
Get.isDarkMode ? const Color(0xFF1E1E1E) : const Color(0xFFFFFFFF);
Color get _kText =>
Get.isDarkMode ? const Color(0xFFF5F5F7) : const Color(0xFF1C1C1E);
Color get _kSubtext =>
Get.isDarkMode ? Colors.white60 : const Color(0xFF6B7280);
Color get _kGreen => const Color(0xFF34A853);
Color get _kGlassSurface => Get.isDarkMode
? Colors.black.withOpacity(0.7)
: Colors.white.withOpacity(0.92);
Color get _kGlassBorder => Get.isDarkMode
? Colors.white.withOpacity(0.12)
: Colors.white.withOpacity(0.5);
class NavigationView extends StatelessWidget {
const NavigationView({super.key});
@@ -33,7 +43,9 @@ class NavigationView extends StatelessWidget {
onMapCreated: c.onMapCreated,
onStyleLoadedCallback: c.onStyleLoaded,
onMapLongClick: c.onMapLongPressed,
styleString: "assets/style.json",
styleString: Get.isDarkMode
? "assets/style_dark.json"
: "assets/style.json",
initialCameraPosition: CameraPosition(
target: c.myLocation ?? const LatLng(33.5138, 36.2765),
zoom: 16.0,
@@ -106,7 +118,7 @@ class _SearchBar extends StatelessWidget {
controller: controller.placeDestinationController,
onChanged: controller.onSearchChanged,
textInputAction: TextInputAction.search,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: _kText,
fontWeight: FontWeight.w500),
@@ -167,8 +179,10 @@ class _SearchResults extends StatelessWidget {
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: controller.placesDestination.length,
separatorBuilder: (_, __) =>
Divider(height: 1, color: Colors.grey[100], indent: 56),
separatorBuilder: (_, __) => Divider(
height: 1,
color: Get.isDarkMode ? Colors.white12 : Colors.grey[100],
indent: 56),
itemBuilder: (_, i) {
final place = controller.placesDestination[i];
final dist = place['distanceKm'] as double?;
@@ -187,8 +201,8 @@ class _SearchResults extends StatelessWidget {
color: _kBlue.withOpacity(0.08),
shape: BoxShape.circle,
),
child: const Icon(Icons.place_rounded,
color: _kBlue, size: 18),
child:
Icon(Icons.place_rounded, color: _kBlue, size: 18),
),
const SizedBox(width: 12),
Expanded(
@@ -196,7 +210,7 @@ class _SearchResults extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(place['name'] ?? '',
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14.5,
color: _kText),
@@ -222,7 +236,7 @@ class _SearchResults extends StatelessWidget {
),
child: Text(
'${dist.toStringAsFixed(1)} كم',
style: const TextStyle(
style: TextStyle(
color: _kBlue,
fontSize: 12,
fontWeight: FontWeight.w600),
@@ -259,11 +273,14 @@ class _TurnBanner extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
child: Container(
decoration: BoxDecoration(
color: _kBlueDark,
color: Get.isDarkMode
? Colors.grey[900]?.withOpacity(0.95)
: _kBlueDark,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: _kBlueDark.withOpacity(0.35),
color: (Get.isDarkMode ? Colors.black : _kBlueDark)
.withOpacity(0.35),
blurRadius: 20,
offset: const Offset(0, 6)),
],
@@ -274,14 +291,14 @@ class _TurnBanner extends StatelessWidget {
children: [
// Turn arrow icon
Container(
width: 52,
height: 52,
width: 64,
height: 64,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.turn_right_rounded,
color: Colors.white, size: 30),
color: Colors.white, size: 40),
),
const SizedBox(width: 14),
@@ -293,16 +310,16 @@ class _TurnBanner extends StatelessWidget {
Text(
controller.distanceToNextStep,
style: const TextStyle(
color: Colors.white70,
fontSize: 13,
fontWeight: FontWeight.w500),
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600),
),
const SizedBox(height: 2),
const SizedBox(height: 4),
Text(
controller.currentInstruction,
style: const TextStyle(
color: Colors.white,
fontSize: 19,
fontSize: 26,
fontWeight: FontWeight.bold,
height: 1.2),
maxLines: 2,
@@ -429,7 +446,7 @@ class _RouteSummaryCard extends StatelessWidget {
left: 0,
right: 0,
child: Container(
decoration: const BoxDecoration(
decoration: BoxDecoration(
color: _kSurface,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
@@ -520,20 +537,20 @@ class _InfoPill extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 15),
const SizedBox(width: 5),
Icon(icon, color: color, size: 22),
const SizedBox(width: 8),
Text(label,
style: TextStyle(
color: color, fontSize: 13.5, fontWeight: FontWeight.w700)),
color: color, fontSize: 18, fontWeight: FontWeight.w800)),
],
),
);
@@ -577,22 +594,27 @@ class _NavigationHUD extends StatelessWidget {
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
color: Get.isDarkMode
? Colors.white.withOpacity(0.05)
: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.withOpacity(0.15)),
border: Border.all(
color: Get.isDarkMode
? Colors.white10
: Colors.grey.withOpacity(0.15)),
),
child: Row(
children: [
Icon(Icons.arrow_forward_rounded,
size: 15, color: _kSubtext),
const SizedBox(width: 8),
size: 20, color: _kSubtext),
const SizedBox(width: 10),
Expanded(
child: Text(
controller.nextInstruction,
style: TextStyle(
color: _kSubtext,
fontSize: 13,
fontWeight: FontWeight.w500),
color: _kText,
fontSize: 16,
fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -636,13 +658,13 @@ class _NavigationHUD extends StatelessWidget {
child: Row(
children: [
const Icon(Icons.stop_rounded,
color: Colors.redAccent, size: 16),
const SizedBox(width: 5),
color: Colors.redAccent, size: 24),
const SizedBox(width: 6),
const Text('إيقاف',
style: TextStyle(
color: Colors.redAccent,
fontSize: 13,
fontWeight: FontWeight.w700)),
fontSize: 18,
fontWeight: FontWeight.bold)),
],
),
),
@@ -669,44 +691,61 @@ class _SpeedBadge extends StatelessWidget {
final bool fast = kmh > 100;
return Positioned(
bottom: MediaQuery.of(context).padding.bottom + 130,
left: 14,
child: Container(
width: 62,
height: 62,
decoration: BoxDecoration(
color: fast ? const Color(0xFFD93025) : _kSurface,
shape: BoxShape.circle,
border: Border.all(
color: fast ? Colors.red.withOpacity(0.3) : Colors.grey[200]!,
width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 12,
offset: const Offset(0, 4)),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$kmh',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: fast ? Colors.white : _kText,
height: 1),
bottom: MediaQuery.of(context).padding.bottom + 150,
left: 16,
child: Stack(
alignment: Alignment.center,
children: [
// Circular progress mimicking a speedometer
SizedBox(
width: 86,
height: 86,
child: CircularProgressIndicator(
value: (kmh / 140.0)
.clamp(0.0, 1.0), // Assuming 140 is max speed shown
strokeWidth: 6,
backgroundColor: Get.isDarkMode
? Colors.white10
: Colors.grey.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(
fast ? Colors.redAccent : _kBlue),
),
Text(
'كم/س',
style: TextStyle(
fontSize: 9,
color: fast ? Colors.white70 : _kSubtext,
fontWeight: FontWeight.w500),
),
Container(
width: 74,
height: 74,
decoration: BoxDecoration(
color: fast ? const Color(0xFFD93025) : _kSurface,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 6)),
],
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$kmh',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.w900,
color: fast ? Colors.white : _kText,
height: 1),
),
Text(
'كم/س',
style: TextStyle(
fontSize: 13,
color: fast ? Colors.white70 : _kSubtext,
fontWeight: FontWeight.w600),
),
],
),
),
],
),
);
}
@@ -735,7 +774,7 @@ class _LoadingOverlay extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(_kBlue),
strokeWidth: 3,
),
@@ -777,14 +816,14 @@ class _GlassCard extends StatelessWidget {
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92),
color: _kGlassSurface,
borderRadius: BorderRadius.circular(borderRadius),
border: Border.all(color: Colors.white.withOpacity(0.5)),
border: Border.all(color: _kGlassBorder),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.07),
color: Colors.black.withOpacity(Get.isDarkMode ? 0.4 : 0.07),
blurRadius: 16,
offset: const Offset(0, 4)),
offset: const Offset(0, 8)),
],
),
padding: padding,