Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
## [1.4.0] - 2026-10-22

@a-nisi-insideapp

- The row actions of the activities list are now `cellHeight` squares (24px by default) instead of the default 40/48px `IconButton`, so the titles keep a usable width on narrow layouts (#32). Values set in `GanttActivityAction.style` still win over the compact size.
- Add `Gantt.activitiesListMinWidth`: the activities list keeps its flex share of the width but never gets narrower than this, capped at half of the width so the grid area keeps at least as much room. Defaults to `null` (flex ratios only).
- Add `Gantt.collapseActionsBelowWidth`: below this width of the activities list, the rows with more than one action show them in a single "more" menu, each with its icon and its tooltip as the label, so those actions need a tooltip (asserted in debug mode). Defaults to `null` (always buttons).
- Add `GanttTheme.listIndent`: the indent each nesting level adds in the activities list. Defaults to `null`, which keeps the current indent growing with the depth (8, 24, 48...).
- Add `GanttActivity.progress` (0.0–1.0) and `GanttActivity.progressColor`: the default cell draws the progress as a line along its bottom edge, over a track that spans the activity, so it reads as a progress bar (the track alone at 0%). Track and line are inset 8px from the real start and end of the activity, in line with the title, and are measured on the whole activity: on a side cut by the visible range they continue beyond the grid edge instead of stopping at it. The default line color is the same as the title text, the track uses the line color at 30% opacity.
- The title of the default cell is now white or black, whichever has the higher contrast against the cell color (at least 4.5:1, WCAG 1.4.3), instead of the app text color, which could be unreadable on colored cells (e.g. white on yellow). Only text without a color of its own takes it.
- The resize edges of a cell use the track color instead of a fixed translucent white, so they show on light cells too, and show the left-right resize cursor (the start edge showed the right one). On a side cut by the visible range the edge is no longer drawn, since the real start or end is out of sight.
- Fix the chart not following a new `theme` (e.g. switching the app between light and dark): `Gantt` now applies the theme it is rebuilt with in `didUpdateWidget`.
- `GanttTheme` now has value equality (`==` and `hashCode`), so a theme rebuilt with the same values no longer counts as a change.
- Add `GanttController.setTheme(theme, {notify})`, like `setHolidays`, to set the theme without notifying the listeners while a build is in progress.
- Fix `GanttController.removeAllFetchListener()` (also called by `dispose()`) throwing a `ConcurrentModificationError` when a fetch listener was registered, e.g. when the controller was disposed while its `Gantt` was still mounted: it removed the listeners one by one while iterating over them.

## [1.3.0] - 2026-09-22

@a-nisi-insideapp
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,16 @@ Gantt(

The main chart container with these key properties:

| Property | Type | Description |
|---------------|------------------------|---------------------------------|
| `startDate` | DateTime | Initial visible date |
| `activities` | List<GanttActivity> | Activities to display |
| `holidays` | List<GanttDateHoliday> | Special dates to highlight |
| `theme` | GanttTheme | Visual customization |
| `controller` | GanttController | Programmatic control |
| `showIsoWeek` | bool | Enables the ISO week-number row |
| Property | Type | Description |
|-----------------------------|------------------------|----------------------------------------------------------|
| `startDate` | DateTime | Initial visible date |
| `activities` | List<GanttActivity> | Activities to display |
| `holidays` | List<GanttDateHoliday> | Special dates to highlight |
| `theme` | GanttTheme | Visual customization |
| `controller` | GanttController | Programmatic control |
| `showIsoWeek` | bool | Enables the ISO week-number row |
| `activitiesListMinWidth` | double? | Minimum width of the activities list |
| `collapseActionsBelowWidth` | double? | List width below which row actions go in a "more" menu |

#### `GanttActivity`

Expand All @@ -91,6 +93,7 @@ GanttActivity(
title: 'Task Name',
color: Colors.blue,
// Optional:
progress: 0.4, // drawn as a line along the bottom of the cell
children: [/* sub-tasks */],
onCellTap: (activity) => print('Tapped ${activity.title}'),
)
Expand Down
11 changes: 11 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class _MyHomePageState extends State<MyHomePage> {
title: 'Subtask 1',
tooltip: 'WO-1001-1 | Subtask',
color: const Color(0xFF81C784),
progress: 0.6, // Drawn as a line over a track along the bottom
actions: [
GanttActivityAction(
icon: Icons.check,
Expand All @@ -140,6 +141,7 @@ class _MyHomePageState extends State<MyHomePage> {
title: 'Subtask 2',
tooltip: 'WO-1001-2 | Subtask with nested children',
color: const Color(0xFF9575CD),
progress: 0, // Not started: only the track is drawn
actions: [
GanttActivityAction(
icon: Icons.add,
Expand Down Expand Up @@ -191,6 +193,7 @@ class _MyHomePageState extends State<MyHomePage> {
title: 'Independent Task',
tooltip: 'A separate task',
color: const Color(0xFF64B5F6),
progress: 0.3,
),

// ✅ Activity from one month ago
Expand All @@ -211,6 +214,7 @@ class _MyHomePageState extends State<MyHomePage> {
title: 'Older Task',
tooltip: 'Past task',
color: const Color(0xFFBCAAA4), // Light brown
progress: 1.2, // Above 1.0 the line is full
),

// ✅ Future activity
Expand All @@ -232,6 +236,8 @@ class _MyHomePageState extends State<MyHomePage> {
'Ongoing Project [Long task] Lorem ipsum dolor sit amet consectetur adipiscing elit quisque faucibus ex sapien vitae pellentesque sem placerat in id cursus mi pretium tellus duis convallis tempus leo eu aenean sed diam.',
tooltip: 'Spanning multiple weeks',
color: const Color(0xFF4FC3F7), // Sky blue
// Past the visible range: the track continues beyond the grid edge
progress: 0.45,
),
];
}
Expand Down Expand Up @@ -273,6 +279,11 @@ class _MyHomePageState extends State<MyHomePage> {
activitiesAsync: (startDate, endDate, activity) async =>
_activities,
showIsoWeek: true,
// Optional: keeps the titles readable on narrow layouts
activitiesListMinWidth: 220,
// Optional: below this list width, rows with more actions
// show them in a single "more" menu
collapseActionsBelowWidth: 260,
holidaysAsync: (startDate, endDate, holidays) async {
final currentYear = DateTime.now().year;
return <GantDateHoliday>[
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.3.0"
version: "1.4.0"
flutter_lints:
dependency: "direct dev"
description:
Expand Down
24 changes: 23 additions & 1 deletion lib/src/classes/activity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ class GanttActivity<T> {
/// Whether to show the activity cell.
final bool showCell;

/// The share of the activity that is complete, from 0.0 to 1.0.
///
/// Drawn by the default cell as a line along the bottom edge, over a track
/// that spans the whole activity, so it reads as a progress bar. Track and
/// line are inset 8px from the real start and end of the activity, in line
/// with the title, and are measured on the whole activity: they stay in
/// place while the visible range moves and, on a side cut by it, continue
/// beyond the grid edge. Values above 1.0 draw a full line; 0.0 draws only
/// the track. If `null` (the default), neither is drawn. Cells built with
/// [builder] or [cellBuilder] do not draw it.
final double? progress;

/// The color of the [progress] line; the track uses it at 30% opacity.
///
/// If `null`, white or black is used, whichever has the higher contrast
/// against the cell, like the title text. A custom color is drawn as is, so
/// its contrast against the cell is up to the caller.
final Color? progressColor;

/// Builder function for custom cell rendering.
final Widget Function(GanttActivity activity)? builder;

Expand Down Expand Up @@ -115,6 +134,8 @@ class GanttActivity<T> {
this.color,
this.actions,
this.showCell = true,
this.progress,
this.progressColor,
this.builder,
this.data,
this.limitStart,
Expand All @@ -123,7 +144,8 @@ class GanttActivity<T> {
start.toDate.isBeforeOrSame(end.toDate) &&
((title == null) != (titleWidget == null)) &&
((cellBuilder == null) || (builder == null)) &&
((listTitle == null) || (listTitleWidget == null)),
((listTitle == null) || (listTitleWidget == null)) &&
(progress == null || progress >= 0),
) {
this.start = start.toDate;
this.end = end.toDate;
Expand Down
45 changes: 45 additions & 0 deletions lib/src/classes/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ class GanttTheme {
/// Defaults to 30.0.
final double dayMinWidth;

/// The indent each nesting level adds in the activities list, in pixels.
///
/// Top-level activities keep a left padding of 8.0 and each child level adds
/// exactly this amount. If `null` (the default), each level adds
/// `8.0 * (level + 1)`, so the indent grows with the depth (8, 24, 48...).
final double? listIndent;

static const double _defaultCellHeight = 24.0;
static const double _defaultRowPadding = 4.0;
static const double _defaultRowsGroupPadding = 16.0;
Expand All @@ -68,6 +75,7 @@ class GanttTheme {
this.rowsGroupPadding = _defaultRowsGroupPadding,
this.headerHeight = _defaultHeaderHeight,
this.dayMinWidth = _defaultDayMinWidth,
this.listIndent,
});

/// Creates a [GanttTheme] based on the current [Theme] of the [BuildContext].
Expand All @@ -89,6 +97,7 @@ class GanttTheme {
double rowsGroupPadding = _defaultRowsGroupPadding,
double headerHeight = _defaultHeaderHeight,
double dayMinWidth = _defaultDayMinWidth,
double? listIndent,
}) {
final colorScheme = Theme.of(context).colorScheme;

Expand All @@ -104,9 +113,45 @@ class GanttTheme {
rowsGroupPadding: rowsGroupPadding,
headerHeight: headerHeight,
dayMinWidth: dayMinWidth,
listIndent: listIndent,
);
}

/// The border radius for activity cells, calculated as 1/3 of [cellHeight].
double get cellRounded => cellHeight / 3;

/// Two themes are equal when all their values are, so a theme rebuilt with
/// the same values does not count as a change.
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GanttTheme &&
other.backgroundColor == backgroundColor &&
other.holidayColor == holidayColor &&
other.weekendColor == weekendColor &&
other.todayBackgroundColor == todayBackgroundColor &&
other.todayTextColor == todayTextColor &&
other.defaultCellColor == defaultCellColor &&
other.cellHeight == cellHeight &&
other.rowPadding == rowPadding &&
other.rowsGroupPadding == rowsGroupPadding &&
other.headerHeight == headerHeight &&
other.dayMinWidth == dayMinWidth &&
other.listIndent == listIndent;

@override
int get hashCode => Object.hash(
backgroundColor,
holidayColor,
weekendColor,
todayBackgroundColor,
todayTextColor,
defaultCellColor,
cellHeight,
rowPadding,
rowsGroupPadding,
headerHeight,
dayMinWidth,
listIndent,
);
}
50 changes: 50 additions & 0 deletions lib/src/utils/color.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';

import '../classes/activity.dart';
import '../classes/theme.dart';

/// Extension methods for [Color] used in the Gantt chart.
extension ColorEx on Color {
/// White or black, whichever has the higher contrast against this color.
///
/// The better of the two reaches at least 4.58:1 on any color, above the
/// 4.5:1 WCAG 1.4.3 asks for text. [ThemeData.estimateBrightnessForColor] is
/// not used: it favors white and leaves it below 4.5:1 on mid tones.
Color get onColor =>
contrastWith(Colors.white) >= contrastWith(Colors.black)
? Colors.white
: Colors.black;

/// WCAG contrast ratio between this color and [other], from 1 to 21.
double contrastWith(Color other) {
final la = computeLuminance();
final lb = other.computeLuminance();
return la > lb ? (la + 0.05) / (lb + 0.05) : (lb + 0.05) / (la + 0.05);
}
}

/// The colors the default cell and its edges draw for a [GanttActivity].
extension GanttActivityColors on GanttActivity {
/// The cell color, falling back to the theme default.
Color cellColor(GanttTheme theme) => color ?? theme.defaultCellColor;

/// White or black against the cell, for the title text.
///
/// The cell color is first blended over the theme background, since a
/// translucent cell looks darker on a dark background. The hover is left
/// out, so the color does not flip while the pointer is over the cell.
Color onCellColor(GanttTheme theme) =>
Color.alphaBlend(cellColor(theme), theme.backgroundColor).onColor;

/// The progress line color: [GanttActivity.progressColor], or the same as
/// the title text so the line and the title read as one.
Color progressLineColor(GanttTheme theme) =>
progressColor ?? onCellColor(theme);

/// The progress line color at 30%, for the track under the line and the
/// resize edges.
Color trackColor(GanttTheme theme) {
final line = progressLineColor(theme);
return line.withValues(alpha: line.a * 0.3);
}
}
Loading
Loading