textual icon indicating copy to clipboard operation
textual copied to clipboard

Date Picker

Open willmcgugan opened this issue 1 year ago • 26 comments

We need a nice date picker.

I see it working in a similar manner to select. See how browsers do it for inspiration.

Discuss design with @willmcgugan before attempting.

We will also need a complimentary time picker. I suspect we should tackle that after the date picker.

willmcgugan avatar Oct 23 '23 14:10 willmcgugan

When I think of a typical "date picker", this includes a text input and a calendar view which opens after clicking an icon.

image

Perhaps DatePicker should be a standalone widget that is the calender view, that can be used with a new DateField widget that opens the calendar view on a new layer?

TomJGooding avatar Oct 24 '23 12:10 TomJGooding

I 100% second the idea of it being a two-parter; I can absolutely envisage wanting the calendar as an always-there widget in some application, as opposed to a drop-down.

davep avatar Oct 24 '23 12:10 davep

Several months ago now I actually had a play at implementing a date picker. It is very rough and doesn't seem to work with newer Textual versions, but here's a demo:

datepicker-demo

TomJGooding avatar Oct 24 '23 12:10 TomJGooding

Yay #team-iso-8601-for-life 👍

davep avatar Oct 24 '23 12:10 davep

Agree with keeping them distinct.

I also agree that it's useful to be able to type the date into the input too rather than having to navigate a widget, particularly if I'm selecting a date long in the past. I don't want to scroll through a Select to choose a year - this is actually another good use case for the ability to type to jump to an option in a Select.

darrenburns avatar Oct 24 '23 12:10 darrenburns

Dropping my two cents, which appear to be aligned with Darren's last remark: I always want to be able to type. Regardless of how the days/months/years are presented as choices, I want to be able to type the date I want (possibly in separate fields).

rodrigogiraoserrao avatar Oct 24 '23 15:10 rodrigogiraoserrao

I'd like to have a stab at adding this DatePicker, if everyone agrees on the calendar design?

But I'm afraid I'd need some guidance, as this would be a complex widget! Obviously I want to be a help rather than a hindrance, so let me know if you would welcome a PR.

TomJGooding avatar Oct 26 '23 21:10 TomJGooding

@TomJGooding Sorry, did mean to get back to you on this. This sounds like a good plan to us. In the first instance, do you want to give a longer proposal for how you want to approach this? If you are keen and happy to this this on and take it through to a full PR (with our help where needed, of course -- I'm happy to help here, in an actual PR, on Discord, even on an actual call 😱), we're happy to do that.

Let me know if you're still interested and I'll assign the issue to you.

davep avatar Oct 31 '23 11:10 davep

Thanks Dave, no problem I understand you have a lot going on at the moment!

I think everyone is in agreement that the datepicker calendar view should be a distinct widget, so this would also need a seperate widget to allow typing the date? Here's a quick sketch of how I envisage this:

DatePicker

This would be a compound widget consisting of:

  • Buttons to navigate previous/next month
  • Label to show current month*
  • Buttons to navigate previous/next year
  • Label to show current year*
  • A DataTable to display the calendar for the month

I'm not sure if this should also include something to display a formatted date, similar to my example above?

*Or perhaps these labels could be buttons, which when clicked replaces the calendar with a new table to select the desired month or year?

DatePickerInput (widget name TBD!)

This would be a compound widget consisting of:

  • Input to type the date (I'm afraid I'd need some advice on date format masking)
  • Button which when clicked will open the DatePicker calendar view
  • DatePicker on an overlay (similar to the Select widget)

Initial Questions

I'm sure I will need some guidance once I start looking at this in more detail, but here's a few initial questions I have:

  • What should the type of DatePicker value be?
  • Would you consider a new dependency for ease of relative date calculations (e.g. python-dateutil)?
  • Should messages bubble up from the compound widgets?

[Edit: I've also realised that the date input might overlap with #3508?]

TomJGooding avatar Oct 31 '23 12:10 TomJGooding

A DataTable to display the calendar for the month

I wonder about the idea of using DataTable for this. If I were to tackle this I think I'd chiefly break it down as you've done here, but with one extra level.

So, at the most atomic level: a month calendar widget, likely built first as a calendar renderable (some other widgets in Textual are renderables at their core), with the widget part being the wrapper that displays that renderable and provides an API for navigation and update[^1].

Then the compound widget you mention, which has all of the extra controls for easier navigation

And then, finally, the full picker input widget.

I would suspect that getting the API interface for that lowest-level widget, that wraps the renderable, is key and then everything else will follow on; the idea here being that the lower-levels will provide flexible APIs that allow building the higher levels, but which also let devs build their own date input systems too by taking the lower-level parts and gluing them together in different ways.

I'd also suggest we approach this with distinct PRs. So how about we kick this off with that renderable and go from there (perhaps the renderable plus lowest-level widget combo; as I can also see that the widget and renderable will each inform the other's design to start with)?

As for another dependency, I would have hoped that the date calculations would be doable with core Python code. Perhaps the calendar module will be helpful here?

[^1]: Some thought might need to be given to how this handles size within a layout -- does it have a minimum dimension, or fixed, etc?

davep avatar Oct 31 '23 13:10 davep

Thanks, I appreciate your advice on how to tackle this!

My idea of using the DataTable for the calendar was just to save having to reinvent the wheel in terms of rows/columns and the cell (i.e. day) hovered/highlighted/selected.

I'm happy to look at starting from a lower level though, if you think that is the better approach.

TomJGooding avatar Oct 31 '23 16:10 TomJGooding

Aye; we're not massively opposed to using the DataTable for it, but it does seem that having a calendar renderable could be useful in a few places longer-term.

davep avatar Oct 31 '23 16:10 davep

Could you explain the benefits of a creating a separate 'renderable' for the month calendar, rather than using the existing DataTable widget?

Ultimately a calendar is just a table. There's over 2,500 LOC in _data_table.py, so I just want to understand exactly what you mean before reinventing the wheel.

TomJGooding avatar Nov 01 '23 19:11 TomJGooding

After thinking more about this, I'm falling into the "we should use the DataTable" camp. It'd take care of a lot of things that you would essentially need to reimplement from the DataTable.

  • Cursor navigation
  • Messages already get posted for selection, highlighting etc, they'd just need to be caught and changed into calendar specific messages
  • Mouse support (including hover)
  • Row/column/cell keys would let you, given a value typed into an input, quickly jump to a cell corresponding to a date with no extra logic required.
  • Component classes are already there (but perhaps there'd need to be some extra work to hide the fact that there's a DataTable underneath)
  • A read-only mode would be simpler (switch off the DataTable cursor)

I'm actually struggling to think of benefits of the renderable approach other than some extra control over the styling. I'm not sure how beneficial that extra control would be though as I can't think of anything it'd let us achieve that we couldn't achieve using a DataTable.

(And just for transparency @TomJGooding, we (myself, @davep, and @willmcgugan) did chat about this briefly in the office and I think we all kind fell into the renderable approach. I only changed my mind after letting it stew in my mind for a bit.)

darrenburns avatar Nov 01 '23 21:11 darrenburns

I was thinking dedicated renderable originally, but I think you may be right about the data table. Can't think of a reason not to use it, if it gets us there faster.

willmcgugan avatar Nov 01 '23 21:11 willmcgugan

I think I'm 51/49 in favour of a renderable, and it's probably how I'd run at it if I were doing it; but I won't claim there's huge benefits; it feels like the most atomic approach - lets folk slap a static calendar in a Static, or even an OptionList (I mean, gods no what monster would, but still...), or in some other widget I've not even considered yet.

But I don't have a massively compelling case for it; just more that we had consensus on Tuesday and I liked the idea. Felt right.

If DataTable works well and consensus has changed I'm good with it.

davep avatar Nov 01 '23 22:11 davep

Thanks everyone for your input, especially @darrenburns for putting flesh on the bones of my concerns.

@davep I understand where you're coming from, but having to reimplement all the existing DataTable functionality surely outweighs any benefits?

lets folk slap a static calendar in a Static,

import calendar
import datetime

from textual.app import App, ComposeResult
from textual.widgets import Static


class StaticCalendarApp(App):
    def compose(self) -> ComposeResult:
        this_year = datetime.date.today().year
        this_month = datetime.date.today().month
        yield Static(calendar.month(this_year, this_month))


if __name__ == "__main__":
    app = StaticCalendarApp()
    app.run()

TomJGooding avatar Nov 01 '23 22:11 TomJGooding

Heh! I was thinking with slightly more decoration. 😛

davep avatar Nov 01 '23 23:11 davep

How would that decoration be defined? Also taking into account any hovering, highlighting, component classes, etc?

TomJGooding avatar Nov 01 '23 23:11 TomJGooding

@TomJGooding Given that everyone seems to be in agreement that it should be a DataTable after all, this is more for "what if" purposes than "but you could still"...

The Bar renderable might serve as one simple example. The styling would be passed in as values and/or made available via properties, and I would imagine that the necessary support for clicking and hovering would be done with meta styling. As for what styles and properties there might be: things like the colour for any row/column headings (day of week, week number, that sort of thing I guess), perhaps different styles for the dates for days of the week, or alternating weeks, etc. I think I'd also have been tempted to have a styling dictionary for individual dates and/or days of months but not particular years (so it's easy to highlight individual dates, or recurring dates -- think highlighting calendar data within the days). Doubtless it could get quite comprehensive and beyond.

Then any wrapping widget would use component classes to bridge the styling from CSS to the renderable (perhaps not all, as I could envisage styling for calendrical data being purely data-driven).

davep avatar Nov 02 '23 08:11 davep

Component classes are already there (but perhaps there'd need to be some extra work to hide the fact that there's a DataTable underneath)

I hadn't fully considered the component classes to be fair, which perhaps is a reason for having a seperate renderable after all... Although I still don't like the idea of reimplementing all the existing table functionality!

Unless there's a non-hackish way of somehow substituting the datatable--header styles with the styles defined in month-calendar--header?

TomJGooding avatar Nov 03 '23 16:11 TomJGooding

I'm not aware of any nice way of substituting the component styles. Maybe we need a mechanism for it 🤔

darrenburns avatar Nov 06 '23 10:11 darrenburns

This is my quick and dirty workaround!

from textual.app import App, ComposeResult
from textual.widget import Widget
from textual.widgets import DataTable, Footer


class DataTableWrapper(Widget):
    COMPONENT_CLASSES = {"datatable-wrapper--header"}

    DEFAULT_CSS = """
    DataTableWrapper > .datatable-wrapper--header {
        background: yellow;
        color: magenta;
    }
    """

    def compose(self) -> ComposeResult:
        yield DataTable()
        yield Footer()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        columns = ["A", "B", "C"]
        table.add_columns(*columns)
        for i in range(1, 4):
            table.add_row(*[f"{col}{i}" for col in columns])

        def update_table_component_styles() -> None:
            style_map = {"datatable--header": "datatable-wrapper--header"}
            for key, value in style_map.items():
                table._component_styles[key] = self.get_component_styles(value)
            table._update_count += 1
            table.refresh()

        table.call_after_refresh(update_table_component_styles)


class ExampleApp(App):
    def compose(self) -> ComposeResult:
        yield DataTableWrapper()


if __name__ == "__main__":
    app = ExampleApp()
    app.run()

TomJGooding avatar Nov 06 '23 23:11 TomJGooding

I was playing with an "infinitely scrollable" calendar sometime in October. Still a work in progress currently on pause.

I used a datatable, and had implemented a kind of DateCell renderable that styles the date depending on a number of factors. I talked about this stuff on discord.

Here's a screenshot of what I had (the first day of every month is highlighted):

image

Anyway, one idea that I wanted to throw in the mix here and something that I tried with my calendar was this: providing a callable to the widget for custom formatting dates. The callable would take a datetime object and return a style string. When new cells are created the callable is called. The application for that could be highlighting holidays, or every Friday is pizza night, etc.

A related issue, and something that I ran into (somewhat), was the idea of (and need for) having a Formatter class implemented for DataTable.

I like the idea of having the two part date picker 👍. My scrollable calendar was meant to tackle a different sort of problem (think of displaying an entire semester of dates and when you select a date it shows an agenda for that day, etc).

1dancook avatar Nov 08 '23 20:11 1dancook

Anyway, one idea that I wanted to throw in the mix here and something that I tried with my calendar was this: providing a callable to the widget for custom formatting dates. The callable would take a datetime object and return a style string. When new cells are created the callable is called. The application for that could be highlighting holidays, or every Friday is pizza night, etc.

I think this idea is interesting, but perhaps needs revisiting later as it seems this could be pretty complicated. For example, there's several types of events to consider:

  • Single date (e.g. meeting tomorrow)
  • Weekly recurring (e.g. pizza Friday)
  • Monthly recurring (e.g. rent day)
  • Yearly recurring (e.g. Christmas)

Those are just some basic recurring dates, other events could bi-monthly or every three days. Some recurring events might have a start and/or end date. What happens if events overlap?

I'm not suggesting a calendar widget shouldn't allow formatting dates, only perhaps this should be in a later release.

TomJGooding avatar Nov 14 '23 22:11 TomJGooding

I think this idea is interesting, but perhaps needs revisiting later as it seems this could be pretty complicated. For example, there's several types of events to consider:

  • Single date (e.g. meeting tomorrow)

  • Weekly recurring (e.g. pizza Friday)

  • Monthly recurring (e.g. rent day)

  • Yearly recurring (e.g. Christmas)

Those are just some basic recurring dates, other events could bi-monthly or every three days. Some recurring events might have a start and/or end date. What happens if events overlap?

I'm not suggesting a calendar widget shouldn't allow formatting dates, only perhaps this should be in a later release.

You have this method implemented so far:

def _format_day(self, date: datetime.date) -> Text:
    formatted_day = Text(str(date.day), justify="center")
    if date.month != self.month:
        formatted_day.style = "grey37"
    return formatted_day

Now what I mean by providing a callable to the widget:

MonthCalendar(year=2023, month=1, custom_formatter=user_defined_function)
# implement custom_formatter in the constructor for MonthCalendar

Then your _format_day method just needs an adjustment such as:

def _format_day(self, date: datetime.date) -> Text:
    formatted_day = Text(str(date.day), justify="center")

    if date.month == self.month:
        user_style = self.custom_formatter(date)
        if user_style:
            formatted_day.style = user_style
    else:
        formatted_day.style = "grey37"

    return formatted_day

As for what you mentioned about the complexity of it, I agree. All of those ideas like repeating dates and everything are, in my opinion, outside the scope of the widget. The user should implement that logic in a function. So, I passed user_defined_function above and let's say we want every Wednesday to be black on blue:

def user_defined_function(date: datetime.date) -> str | None:
    # this function is not part of textual
    # takes a date and returns a style string or None
    if date.weekday() == 2:
        return "black on blue"
    return None
    

This way a user should implement whatever logic they want. It just provides a hook for the user to format dates based on anything. It could be something like "every third Friday" or it could be "Dates that have a todo" or "Dates that have an event" etc.

1dancook avatar Nov 15 '23 10:11 1dancook