# This file is part of Elide, frontend to Lisien, a framework for life simulation games.
# Copyright (c) Zachary Spector, public@zacharyspector.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Widget that looks like a trading card, and a layout within which it
can be dragged and dropped to some particular position within stacks
of other cards.
"""
import pygments
from kivy.clock import Clock
from kivy.graphics import InstructionGroup
from kivy.logger import Logger
from kivy.properties import (
AliasProperty,
BooleanProperty,
BoundedNumericProperty,
DictProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
ReferenceListProperty,
StringProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.uix.layout import Layout
from kivy.uix.stencilview import StencilView
from kivy.uix.widget import Widget
from kivy.utils import get_hex_from_color
from pygments.formatters.bbcode import BBCodeFormatter
from pygments.lexers import PythonLexer
from .util import load_string_once
dbg = Logger.debug
[docs]
def get_pos_hint_x(poshints, sizehintx):
"""Return ``poshints['x']`` if available, or its computed equivalent
otherwise.
"""
if "x" in poshints:
return poshints["x"]
elif sizehintx is not None:
if "center_x" in poshints:
return poshints["center_x"] - sizehintx / 2
elif "right" in poshints:
return poshints["right"] - sizehintx
[docs]
def get_pos_hint_y(poshints, sizehinty):
"""Return ``poshints['y']`` if available, or its computed equivalent
otherwise.
"""
if "y" in poshints:
return poshints["y"]
elif sizehinty is not None:
if "center_y" in poshints:
return poshints["center_y"] - sizehinty / 2
elif "top" in poshints:
return poshints["top"] - sizehinty
[docs]
def get_pos_hint(poshints, sizehintx, sizehinty):
"""Return a tuple of ``(pos_hint_x, pos_hint_y)`` even if neither of
those keys are present in the provided ``poshints`` -- they can be
computed using the available keys together with ``size_hint_x``
and ``size_hint_y``.
"""
return (
get_pos_hint_x(poshints, sizehintx),
get_pos_hint_y(poshints, sizehinty),
)
[docs]
class ColorTextureBox(Widget):
"""A box, with a background of one solid color, an outline of another
color, and possibly a texture covering the background.
"""
color = ListProperty([1, 1, 1, 1])
outline_color = ListProperty([0, 0, 0, 0])
texture = ObjectProperty(None, allownone=True)
[docs]
class Card(FloatLayout):
"""A trading card with text and illustration
Its appearance is determined by several properties, the most
important being:
* ``headline_text``, a string to be shown at the top of the card;
may be styled with eg. ``headline_font_name`` or
``headline_color``
* ``art_source``, the path to an image to be displayed below the
headline; may be hidden by setting ``show_art`` to ``False``
* ``midline_text``, similar to ``headline_text`` but appearing
below the art
* ``text``, shown in a box the same size as the art. Styleable
like ``headline_text`` and you can customize the box with
eg. ``foreground_color`` and ``foreground_source``
* ``footer_text``, like ``headline_text`` but at the bottom
:class:`Card` is particularly useful when put in a
:class:`DeckLayout`, allowing the user to drag cards in between
any number of piles, into particular positions within a particular
pile, and so forth.
"""
dragging = BooleanProperty(False)
deck = NumericProperty()
idx = NumericProperty()
ud = DictProperty({})
collide_x = NumericProperty()
collide_y = NumericProperty()
collide_pos = ReferenceListProperty(collide_x, collide_y)
foreground = ObjectProperty()
foreground_source = StringProperty("")
foreground_color = ListProperty([1, 1, 1, 1])
foreground_image = ObjectProperty(None, allownone=True)
foreground_texture = ObjectProperty(None, allownone=True)
background_source = StringProperty("")
background_color = ListProperty([0.7, 0.7, 0.7, 1])
background_image = ObjectProperty(None, allownone=True)
background_texture = ObjectProperty(None, allownone=True)
outline_color = ListProperty([0, 0, 0, 1])
content_outline_color = ListProperty([0, 0, 0, 0])
foreground_outline_color = ListProperty([0, 0, 0, 1])
art_outline_color = ListProperty([0, 0, 0, 0])
art = ObjectProperty()
art_source = StringProperty("")
art_color = ListProperty([1, 1, 1, 1])
art_image = ObjectProperty(None, allownone=True)
art_texture = ObjectProperty(None, allownone=True)
show_art = BooleanProperty(True)
headline = ObjectProperty()
headline_text = StringProperty("Headline")
headline_markup = BooleanProperty(True)
headline_font_name = StringProperty("Roboto-Regular")
headline_font_size = NumericProperty(18)
headline_color = ListProperty([0, 0, 0, 1])
midline = ObjectProperty()
midline_text = StringProperty("")
midline_markup = BooleanProperty(True)
midline_font_name = StringProperty("Roboto-Regular")
midline_font_size = NumericProperty(14)
midline_color = ListProperty([0, 0, 0, 1])
footer = ObjectProperty()
footer_text = StringProperty("")
footer_markup = BooleanProperty(True)
footer_font_name = StringProperty("Roboto-Regular")
footer_font_size = NumericProperty(10)
footer_color = ListProperty([0, 0, 0, 1])
text = StringProperty("")
text_color = ListProperty([0, 0, 0, 1])
markup = BooleanProperty(True)
font_name = StringProperty("Roboto-Regular")
font_size = NumericProperty(12)
editable = BooleanProperty(False)
edit_func = ObjectProperty()
def on_text(self, *_):
if "main_text" not in self.ids:
Clock.schedule_once(self.on_text, 0)
return
text = self.text.replace("\t", " ")
if self.markup:
if not hasattr(self, "_lexer"):
self._lexer = PythonLexer()
self._formatter = BBCodeFormatter()
text = (
text.replace("[", "\x01")
.replace("]", "\x02")
.replace("\t", " " * 4)
)
text = pygments.format(
self._lexer.get_tokens(text),
self._formatter,
)
text = text.replace("\x01", "&bl;").replace("\x02", "&br;")
text = "".join(
f"[color={get_hex_from_color(self.text_color)}]{text}[/color]"
)
self.ids.main_text.text = text
[docs]
def on_background_source(self, *args):
"""When I get a new ``background_source``, load it as an
:class:`Image` and store that in ``background_image``.
"""
if self.background_source:
self.background_image = Image(source=self.background_source)
[docs]
def on_background_image(self, *args):
"""When I get a new ``background_image``, store its texture in
``background_texture``.
"""
if self.background_image is not None:
self.background_texture = self.background_image.texture
[docs]
def on_foreground_source(self, *args):
"""When I get a new ``foreground_source``, load it as an
:class:`Image` and store that in ``foreground_image``.
"""
if self.foreground_source:
self.foreground_image = Image(source=self.foreground_source)
[docs]
def on_foreground_image(self, *args):
"""When I get a new ``foreground_image``, store its texture in my
``foreground_texture``.
"""
if self.foreground_image is not None:
self.foreground_texture = self.foreground_image.texture
[docs]
def on_art_source(self, *args):
"""When I get a new ``art_source``, load it as an :class:`Image` and
store that in ``art_image``.
"""
if self.art_source:
self.art_image = Image(source=self.art_source)
[docs]
def on_art_image(self, *args):
"""When I get a new ``art_image``, store its texture in
``art_texture``.
"""
if self.art_image is not None:
self.art_texture = self.art_image.texture
[docs]
def on_touch_down(self, touch):
"""If I'm the first card to collide this touch, grab it, store my
metadata in its userdict, and store the relative coords upon
me where the collision happened.
"""
if not self.collide_point(*touch.pos):
return
if "card" in touch.ud:
return
if self.editable and self.ids.editbut.collide_point(*touch.pos):
touch.grab(self.ids.editbut)
self.ids.editbut.dispatch("on_touch_down", touch)
return
touch.grab(self)
self.dragging = True
touch.ud["card"] = self
touch.ud["idx"] = self.idx
touch.ud["deck"] = self.deck
touch.ud["layout"] = self.parent
self.collide_x = touch.x - self.x
self.collide_y = touch.y - self.y
[docs]
def on_touch_move(self, touch):
"""If I'm being dragged, move so as to be always positioned the same
relative to the touch.
"""
if not self.dragging:
touch.ungrab(self)
return
self.pos = (touch.x - self.collide_x, touch.y - self.collide_y)
[docs]
def on_touch_up(self, touch):
"""Stop dragging if needed."""
if not self.dragging:
return
touch.ungrab(self)
self.dragging = False
[docs]
def copy(self):
"""Return a new :class:`Card` just like me."""
d = {}
for att in (
"deck",
"idx",
"ud",
"foreground_source",
"foreground_color",
"foreground_image",
"foreground_texture",
"background_source",
"background_color",
"background_image",
"background_texture",
"outline_color",
"content_outline_color",
"foreground_outline_color",
"art_outline_color",
"art_source",
"art_color",
"art_image",
"art_texture",
"show_art",
"headline_text",
"headline_markup",
"headline_font_name",
"headline_font_size",
"headline_color",
"midline_text",
"midline_markup",
"midline_font_name",
"midline_font_size",
"midline_color",
"footer_text",
"footer_markup",
"footer_font_name",
"footer_font_size",
"footer_color",
"text",
"text_color",
"markup",
"font_name",
"font_size",
"editable",
"on_edit",
):
v = getattr(self, att)
if v is not None:
d[att] = v
return Card(**d)
[docs]
class Foundation(ColorTextureBox):
"""An empty outline to indicate where a deck is when there are no
cards in it.
"""
color = ListProperty([])
"""Color of the outline"""
deck = NumericProperty(0)
"""Index of the deck in the parent :class:`DeckLayout`"""
[docs]
def upd_pos(self, *args):
"""Ask the foundation where I should be, based on what deck I'm
for.
"""
self.pos = self.parent._get_foundation_pos(self.deck)
[docs]
def upd_size(self, *args):
"""I'm the same size as any given card in my :class:`DeckLayout`."""
self.size = (
self.parent.card_size_hint_x * self.parent.width,
self.parent.card_size_hint_y * self.parent.height,
)
[docs]
class DeckBuilderLayout(Layout):
"""Sizes and positions :class:`Card` objects based on their order
within ``decks``, a list of lists where each sublist is a deck of
cards.
"""
direction = OptionProperty(
"ascending", options=["ascending", "descending"]
)
"""Should the beginning card of each deck appear on the bottom
('ascending'), or the top ('descending')?
"""
card_size_hint_x = BoundedNumericProperty(1, min=0, max=1)
"""Each card's width, expressed as a proportion of my width."""
card_size_hint_y = BoundedNumericProperty(1, min=0, max=1)
"""Each card's height, expressed as a proportion of my height."""
card_size_hint = ReferenceListProperty(card_size_hint_x, card_size_hint_y)
"""Size hint of cards, relative to my size."""
starting_pos_hint = DictProperty({"x": 0, "y": 0})
"""Pos hint at which to place the initial card of the initial deck."""
card_x_hint_step = NumericProperty(0)
"""Each time I put another card on a deck, I'll move it this much of
my width to the right of the previous card.
"""
card_y_hint_step = NumericProperty(-1)
"""Each time I put another card on a deck, I'll move it this much of
my height above the previous card.
"""
card_hint_step = ReferenceListProperty(card_x_hint_step, card_y_hint_step)
"""An offset, expressed in proportion to my size, applied to each
successive card in a given deck.
"""
deck_x_hint_step = NumericProperty(1)
"""When I start a new deck, it will be this far to the right of the
previous deck, expressed as a proportion of my width.
"""
deck_y_hint_step = NumericProperty(0)
"""When I start a new deck, it will be this far above the previous
deck, expressed as a proportion of my height.
"""
deck_hint_step = ReferenceListProperty(deck_x_hint_step, deck_y_hint_step)
"""Offset of each deck with respect to the previous, as a proportion
of my size.
"""
decks = ListProperty([[]]) # list of lists of cards
"""Put a list of lists of :class:`Card` objects here and I'll position
them appropriately. Please don't use ``add_widget``.
"""
deck_x_hint_offsets = ListProperty([])
"""An additional proportional x-offset for each deck, defaulting to 0."""
deck_y_hint_offsets = ListProperty([])
"""An additional proportional y-offset for each deck, defaulting to 0."""
foundation_color = ListProperty([1, 1, 1, 1])
"""Color to use for the outline showing where a deck is when it's
empty.
"""
insertion_deck = BoundedNumericProperty(None, min=0, allownone=True)
"""Index of the deck that a card is being dragged into."""
insertion_card = BoundedNumericProperty(None, min=0, allownone=True)
"""Index within the current deck that a card is being dragged into."""
_foundations = ListProperty([])
"""Private. A list of :class:`Foundation` widgets, one per deck."""
def __init__(self, **kwargs):
"""Bind most of my custom properties to ``_trigger_layout``."""
super().__init__(**kwargs)
self.bind(
card_size_hint=self._trigger_layout,
starting_pos_hint=self._trigger_layout,
card_hint_step=self._trigger_layout,
deck_hint_step=self._trigger_layout,
decks=self._trigger_layout,
deck_x_hint_offsets=self._trigger_layout,
deck_y_hint_offsets=self._trigger_layout,
insertion_deck=self._trigger_layout,
insertion_card=self._trigger_layout,
)
def _get_foundation_pos(self, i):
"""Private. Get the absolute coordinates to use for a deck's
foundation, based on the ``starting_pos_hint``, the
``deck_hint_step``, ``deck_x_hint_offsets``, and
``deck_y_hint_offsets``.
"""
(phx, phy) = get_pos_hint(self.starting_pos_hint, *self.card_size_hint)
phx += self.deck_x_hint_step * i + self.deck_x_hint_offsets[i]
phy += self.deck_y_hint_step * i + self.deck_y_hint_offsets[i]
x = phx * self.width + self.x
y = phy * self.height + self.y
return (x, y)
def _get_foundation(self, i):
"""Return a :class:`Foundation` for some deck, creating it if
needed.
"""
if i >= len(self._foundations) or self._foundations[i] is None:
oldfound = list(self._foundations)
extend = i - len(oldfound) + 1
if extend > 0:
oldfound += [None] * extend
width = self.card_size_hint_x * self.width
height = self.card_size_hint_y * self.height
found = Foundation(
pos=self._get_foundation_pos(i), size=(width, height), deck=i
)
self.bind(
pos=found.upd_pos,
card_size_hint=found.upd_pos,
deck_hint_step=found.upd_pos,
size=found.upd_pos,
deck_x_hint_offsets=found.upd_pos,
deck_y_hint_offsets=found.upd_pos,
)
self.bind(size=found.upd_size, card_size_hint=found.upd_size)
oldfound[i] = found
self._foundations = oldfound
return self._foundations[i]
[docs]
def on_decks(self, *args):
"""Inform the cards of their deck and their index within the deck;
extend the ``_hint_offsets`` properties as needed; and trigger
a layout.
"""
if None in (
self.canvas,
self.decks,
self.deck_x_hint_offsets,
self.deck_y_hint_offsets,
):
Clock.schedule_once(self.on_decks, 0)
return
self.clear_widgets()
decknum = 0
for deck in self.decks:
cardnum = 0
for card in deck:
if not isinstance(card, Card):
raise TypeError("You must only put Card in decks")
if card not in self.children:
self.add_widget(card)
if card.deck != decknum:
card.deck = decknum
if card.idx != cardnum:
card.idx = cardnum
cardnum += 1
decknum += 1
if len(self.deck_x_hint_offsets) < len(self.decks):
self.deck_x_hint_offsets = list(self.deck_x_hint_offsets) + [0] * (
len(self.decks) - len(self.deck_x_hint_offsets)
)
if len(self.deck_y_hint_offsets) < len(self.decks):
self.deck_y_hint_offsets = list(self.deck_y_hint_offsets) + [0] * (
len(self.decks) - len(self.deck_y_hint_offsets)
)
self._trigger_layout()
[docs]
def point_before_card(self, card, x, y):
"""Return whether ``(x, y)`` is somewhere before ``card``, given how I
know cards to be arranged.
If the cards are being stacked down and to the right, that
means I'm testing whether ``(x, y)`` is above or to the left
of the card.
"""
def ycmp():
if self.card_y_hint_step == 0:
return False
elif self.card_y_hint_step > 0:
# stacking upward
return y < card.y
else:
# stacking downward
return y > card.top
if self.card_x_hint_step > 0:
# stacking to the right
if x < card.x:
return True
return ycmp()
elif self.card_x_hint_step == 0:
return ycmp()
else:
# stacking to the left
if x > card.right:
return True
return ycmp()
[docs]
def point_after_card(self, card, x, y):
"""Return whether ``(x, y)`` is somewhere after ``card``, given how I
know cards to be arranged.
If the cards are being stacked down and to the right, that
means I'm testing whether ``(x, y)`` is below or to the left
of ``card``.
"""
def ycmp():
if self.card_y_hint_step == 0:
return False
elif self.card_y_hint_step > 0:
# stacking upward
return y > card.top
else:
# stacking downward
return y < card.y
if self.card_x_hint_step > 0:
# stacking to the right
if x > card.right:
return True
return ycmp()
elif self.card_x_hint_step == 0:
return ycmp()
else:
# stacking to the left
if x < card.x:
return True
return ycmp()
[docs]
def on_touch_move(self, touch):
"""If a card is being dragged, move other cards out of the way to show
where the dragged card will go if you drop it.
"""
if (
"card" not in touch.ud
or "layout" not in touch.ud
or touch.ud["layout"] != self
):
return
if touch.ud["layout"] == self and not hasattr(
touch.ud["card"], "_topdecked"
):
touch.ud["card"]._topdecked = InstructionGroup()
touch.ud["card"]._topdecked.add(touch.ud["card"].canvas)
self.canvas.after.add(touch.ud["card"]._topdecked)
for i, deck in enumerate(self.decks):
cards = [card for card in deck if not card.dragging]
maxidx = max(card.idx for card in cards) if cards else 0
if self.direction == "descending":
cards.reverse()
cards_collided = [
card for card in cards if card.collide_point(*touch.pos)
]
if cards_collided:
collided = cards_collided.pop()
for card in cards_collided:
if card.idx > collided.idx:
collided = card
if collided.deck == touch.ud["deck"]:
self.insertion_card = (
1
if collided.idx == 0
else maxidx + 1
if collided.idx == maxidx
else collided.idx + 1
if collided.idx > touch.ud["idx"]
else collided.idx
)
else:
dropdeck = self.decks[collided.deck]
maxidx = max(card.idx for card in dropdeck)
self.insertion_card = (
1
if collided.idx == 0
else maxidx + 1
if collided.idx == maxidx
else collided.idx + 1
)
if self.insertion_deck != collided.deck:
self.insertion_deck = collided.deck
return
else:
if self.insertion_deck == i:
if self.insertion_card in (0, len(deck)):
pass
elif self.point_before_card(cards[0], *touch.pos):
self.insertion_card = 0
elif self.point_after_card(cards[-1], *touch.pos):
self.insertion_card = cards[-1].idx
else:
for j, found in enumerate(self._foundations):
if found is not None and found.collide_point(
*touch.pos
):
self.insertion_deck = j
self.insertion_card = 0
return
[docs]
def on_touch_up(self, touch):
"""If a card is being dragged, put it in the place it was just dropped
and trigger a layout.
"""
if (
"card" not in touch.ud
or "layout" not in touch.ud
or touch.ud["layout"] != self
):
return
if hasattr(touch.ud["card"], "_topdecked"):
self.canvas.after.remove(touch.ud["card"]._topdecked)
del touch.ud["card"]._topdecked
if None not in (self.insertion_deck, self.insertion_card):
# need to sync to adapter.data??
card = touch.ud["card"]
del card.parent.decks[card.deck][card.idx]
for i in range(0, len(card.parent.decks[card.deck])):
card.parent.decks[card.deck][i].idx = i
deck = self.decks[self.insertion_deck]
if self.insertion_card >= len(deck):
deck.append(card)
else:
deck.insert(self.insertion_card, card)
card.deck = self.insertion_deck
card.idx = self.insertion_card
self.decks[self.insertion_deck] = deck
self.insertion_deck = self.insertion_card = None
self._trigger_layout()
[docs]
def on_insertion_card(self, *args):
"""Trigger a layout"""
if self.insertion_card is not None:
self._trigger_layout()
[docs]
def do_layout(self, *args):
"""Layout each of my decks"""
if self.size == [1, 1]:
return
for i in range(0, len(self.decks)):
self.layout_deck(i)
[docs]
def layout_deck(self, i):
"""Stack the cards, starting at my deck's foundation, and proceeding
by ``card_pos_hint``
"""
def get_dragidx(cards):
j = 0
for card in cards:
if card.dragging:
return j
j += 1
# Put a None in the card list in place of the card you're
# hovering over, if you're dragging another card. This will
# result in an empty space where the card will go if you drop
# it now.
cards = list(self.decks[i])
dragidx = get_dragidx(cards)
if dragidx is not None:
del cards[dragidx]
if self.insertion_deck == i and self.insertion_card is not None:
insdx = self.insertion_card
if dragidx is not None and insdx > dragidx:
insdx -= 1
cards.insert(insdx, None)
if self.direction == "descending":
cards.reverse()
# Work out the initial pos_hint for this deck
(phx, phy) = get_pos_hint(self.starting_pos_hint, *self.card_size_hint)
phx += self.deck_x_hint_step * i + self.deck_x_hint_offsets[i]
phy += self.deck_y_hint_step * i + self.deck_y_hint_offsets[i]
(w, h) = self.size
(x, y) = self.pos
# start assigning pos and size to cards
found = self._get_foundation(i)
if found not in self.children:
self.add_widget(found)
for card in cards:
if card is not None:
if card in self.children:
self.remove_widget(card)
(shw, shh) = self.card_size_hint
card.pos = (x + phx * w, y + phy * h)
card.size = (w * shw, h * shh)
self.add_widget(card)
phx += self.card_x_hint_step
phy += self.card_y_hint_step
[docs]
class DeckBuilderView(DeckBuilderLayout, StencilView):
"""Just a :class:`DeckBuilderLayout` mixed with
:class:`StencilView`.
"""
pass
load_string_once("""
<ColorTextureBox>:
canvas:
Color:
rgba: root.color
Rectangle:
texture: root.texture
pos: root.pos
size: root.size
Color:
rgba: root.outline_color
Line:
points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top, self.x, self.y]
Color:
rgba: [1, 1, 1, 1]
<Foundation>:
color: [0, 0, 0, 0]
outline_color: [1, 1, 1, 1]
<Card>:
headline: headline
midline: midline
footer: footer
art: art
foreground: foreground
canvas:
Color:
rgba: root.background_color
Rectangle:
texture: root.background_texture
pos: root.pos
size: root.size
Color:
rgba: root.outline_color
Line:
points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top, self.x, self.y]
Color:
rgba: [1, 1, 1, 1]
BoxLayout:
size_hint: 0.9, 0.9
pos_hint: {'x': 0.05, 'y': 0.05}
orientation: 'vertical'
canvas:
Color:
rgba: root.content_outline_color
Line:
points: [self.x, self.y, self.right, self.y, self.right, self.top, self.x, self.top, self.x, self.y]
Color:
rgba: [1, 1, 1, 1]
Label:
id: headline
text: root.headline_text
markup: root.headline_markup
font_name: root.headline_font_name
font_size: root.headline_font_size
color: root.headline_color
size_hint: (None, None)
size: self.texture_size
ColorTextureBox:
id: art
color: root.art_color
texture: root.art_texture
outline_color: root.art_outline_color if root.show_art else [0, 0, 0, 0]
size_hint: (1, 1) if root.show_art else (None, None)
size: (0, 0)
Label:
id: midline
text: root.midline_text
markup: root.midline_markup
font_name: root.midline_font_name
font_size: root.midline_font_size
color: root.midline_color
size_hint: (None, None)
size: self.texture_size
ColorTextureBox:
id: foreground
color: root.foreground_color
outline_color: root.foreground_outline_color
texture: root.foreground_texture
Label:
id: main_text
color: root.text_color
markup: root.markup
font_name: root.font_name
font_size: root.font_size
text_size: foreground.size
size_hint: (None, None)
size: self.texture_size
pos: foreground.pos
valign: 'top'
Button:
id: editbut
background_normal: 'atlas://data/images/defaulttheme/button' if root.editable else ''
background_down: 'atlas://data/images/defaulttheme/button_pressed' if root.editable else ''
color: (1., 1., 1., 1.) if root.editable else (0., 0., 0., 0.)
background_color: (1., 1., 1., 1.) if root.editable else (0., 0., 0., 0.)
text_size: self.size
size: self.texture_size
size_hint: (None, None)
font_name: 'DejaVuSans'
font_size: 30
x: foreground.right - self.width - (.1 * self.width)
y: foreground.top - self.height - (.1 * self.height)
text: '✐' if root.editable else ''
on_press: root.edit_func(root)
disabled: not root.editable
Label:
id: footer
text: root.footer_text
markup: root.footer_markup
font_name: root.footer_font_name
font_size: root.footer_font_size
color: root.footer_color
size_hint: (None, None)
size: self.texture_size
<DeckBuilderScrollBar>:
ScrollBarBar:
id: bar
color: root.bar_color if root.scrolling else root.bar_inactive_color
texture: root.bar_texture
""")
if __name__ == "__main__":
deck0 = [
Card(
background_color=[0, 1, 0, 1],
headline_text="Card {}".format(i),
art_color=[1, 0, 0, 1],
midline_text="0deck",
foreground_color=[0, 0, 1, 1],
text="The quick brown fox jumps over the lazy dog",
text_color=[1, 1, 1, 1],
footer_text=str(i),
)
for i in range(0, 9)
]
deck1 = [
Card(
background_color=[0, 0, 1, 1],
headline_text="Card {}".format(i),
art_color=[0, 1, 0, 1],
show_art=False,
midline_text="1deck",
foreground_color=[1, 0, 0, 1],
text="Have a steak at the porter house bar",
text_color=[1, 1, 0, 1],
footer_text=str(i),
)
for i in range(0, 9)
]
from kivy.base import runTouchApp
from kivy.core.window import Window
from kivy.modules import inspector
builder = DeckBuilderLayout(
card_size_hint=(0.15, 0.3),
pos_hint={"x": 0, "y": 0},
starting_pos_hint={"x": 0.1, "top": 0.9},
card_hint_step=(0.05, -0.1),
deck_hint_step=(0.4, 0),
decks=[deck0, deck1],
deck_y_hint_offsets=[0, 1],
)
layout = BoxLayout(orientation="horizontal")
left_bar = DeckBuilderScrollBar(
deckbuilder=builder, orientation="vertical", size_hint_x=0.1, deckidx=0
)
right_bar = DeckBuilderScrollBar(
deckbuilder=builder, orientation="vertical", size_hint_x=0.1, deckidx=1
)
layout.add_widget(left_bar)
layout.add_widget(builder)
layout.add_widget(right_bar)
inspector.create_inspector(Window, layout)
runTouchApp(layout)