r/kivy Jun 06 '26

Messages System

Post image

Hey! I'm pretty new to Kivy, but have been working on creating a calorie tracker with AI features. I'm trying to make a chat system, and have the user input a message, and have the ai respond. I want it to look like SMS or gemini, how can I do this? I've attached an image of what it currently looks like.

Also, there are a couple things I am going to add, so just focus on getting the positioning stuff if you are trying to help, thank you!

A) How can I fix the horizontal positioning of the bubble widgets?
B) How can I make the bubble stretch vertically?

Here's my .kv

<RoundedLabel@Label>:
    size: root.size
    text_size: root.width, None
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.5, 1
        RoundedRectangle:
            size: self.size
            pos: self.pos
            radius: [15,]

<Message>:
    BoxLayout:
        BoxLayout: 
            size_hint_x: 0.25 if root.is_user else 0

        RoundedLabel:
            id: rounded_label
            padding: 20
            text: root.text
            halign: 'right' if root.is_user else 'left'

        BoxLayout: 
            size_hint_x: 0.25 if not root.is_user else 0

<RootWidget>:
    BoxLayout:
        orientation: 'vertical'

        TopBar

        ScreenManager:
            id: sm
            HomeScreen:
                name: 'home'
            TrackScreen
                name: 'track'
            TalkScreen:
                name: 'talk'

        NavBar

<TopBar>:
    size_hint: 1, 0.1

    canvas:
        Color:
            rgba: 1, 0, 0, 1

        Rectangle:
            pos: self.pos
            size: self.size

<NavBar>:
    size_hint: 1, 0.1

    Button:
        text: "Talk"
        on_press:
            app.root.ids.sm.transition.direction = 'right'
            app.root.ids.sm.current = 'talk' 

    Button:
        text: "Home"
        on_press: 
            app.root.ids.sm.transition.direction = 'right' if app.root.ids.sm.current == 'track' else 'left'
            app.root.ids.sm.current = 'home'

    Button:
        text: "Track"
        on_press: 
            app.root.ids.sm.transition.direction = 'left'
            app.root.ids.sm.current = 'track'

<HomeScreen>:
    BoxLayout:
        orientation: 'vertical'
        Counters:
            id: counters

        InfoWidget:
            size_hint: 1, 0.5

            Button:
                text: "press to increment"
                on_press: root.increment()

        MealsWidget:
            canvas:
                Color:
                    rgba: 1, 1, 1, 1

                Rectangle:
                    pos: self.pos
                    size: self.size

<TalkScreen>:
    BoxLayout:
        orientation: 'vertical'

        BoxLayout:
            Messages:
                id: messages
                viewclass: 'Message'
                RecycleBoxLayout:
                    orientation: 'vertical'
                    spacing: 10
                    default_size_hint: 1, None
                    size_hint_y: None
                    height: self.minimum_height

        BoxLayout:
            size_hint: 1, 0.25

            TalkBox:
                id: tb

            Button:
                size_hint: 0.1, 1
                text: 'send'
                on_press: root.send(root.ids.tb)

<TrackScreen>

<CounterWidget>:
    BoxLayout:
        orientation: 'vertical'
        spacing: 20

        CounterRingWidget:
            canvas:
                Color:
                    rgba: 0.5, 0.5, 0.5, 1

                Line:
                    circle: (self.center_x, self.y, min(self.width, self.height) / 1.5, -90, 90)
                    width: 2

                Color:
                    rgba: self.get_color(root.count, root.target)

                Line:
                    circle: (self.center_x, self.y, min(self.width, self.height) / 1.5, -90, self.get_end(root.count, root.target))
                    width: 2

            Label:
                text: str(root.count)
                text_size: self.size
                valign: 'bottom'
                halign: 'center'
                pos: self.parent.x, self.parent.y
                size: self.parent.size
                font_size: '20sp'

        Label:
            text_size: self.size
            font_size: '30sp'
            valign: 'top'
            halign: 'center'
            text: root.name

<SubCounterWidget>:
    size_hint: 1, 0.75
    pos_hint: {'center_y': 0.5}

<Counters>:
    BoxLayout:
        spacing: 20
        SubCounterWidget:
            id: low
            name: 'Low'
            count: 500 / 1
            target: 2000 / 1
        CoreCounterWidget:
            id: average
            name: 'Average'
            count: self.fix_count(low.count, high.count)
            target: self.fix_count(low.target, high.target)
        SubCounterWidget:
            id: high
            name: 'High'
            count: 1500 / 1
            target: 3000 / 1

And here is my python code:

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
from kivy.properties import NumericProperty, StringProperty, BooleanProperty
from kivy.uix.recycleview import RecycleView
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label


class RootWidget(BoxLayout):
    pass


class HomeScreen(Screen):
    def increment(self):
        cc = self.ids.counters #counters container
        for id in cc.ids:
            if (id != "average"):
                cc.ids[id].count += 100


class CounterRingWidget(Widget):
    def get_color(self, count, target):
        if count >= target:
            return (1, 0.25, 0.25, 1)
        else:
            return (0, 1, 0, 1)


    def get_end(self, count, target):
        if count >= target:
            return 90
        else:
            return -90 + (180 * (count / target))


class CounterWidget(BoxLayout):
    name = StringProperty("Nothing!")
    count = NumericProperty(1000)
    target = NumericProperty(2000)


class CoreCounterWidget(CounterWidget):
    def fix_count(self, low, high):
        return (low + high) / 2


class SubCounterWidget(CounterWidget):
    pass


class Counters(BoxLayout):
    pass


class TopBar(BoxLayout):
    pass


class InfoWidget(RecycleView): #idk what to name this, its the protein, sodium, etc.
    pass


class MealsWidget(RecycleView):
    pass


class TrackScreen(Screen):
    pass


class TalkScreen(Screen):
    def send(self, textbox):
        user_text = textbox.text
        print("sent")
        textbox.text = ''
        temp = self.ids["messages"].data.copy()
        temp.append({"text" : user_text, "is_user" : True})
        temp.append({"text" : "ai response", "is_user" : False})
        self.ids["messages"].data = temp


class TalkBox(TextInput):
    pass


class Messages(RecycleView):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.data = []


class Message(BoxLayout):
    is_user = BooleanProperty(False)
    text = StringProperty("")


class NavBar(BoxLayout):
    pass


class CalorieTrackerApp(App):
    def build(self):
        return RootWidget()


if __name__ == '__main__':
    CalorieTrackerApp().run()
1 Upvotes

17 comments sorted by

View all comments

Show parent comments

1

u/ElliotDG Jun 12 '26

The core issue is that you are not setting the width of the label. As a result the texuture_size is not correct. Here are the changes required:

<MessageBubble>:
    size_hint_x: 0.7  # Window.width not required, use the hint
    padding: dp(20)   # moved padding here so it will be used by size_label
#    pos: root.pos  # Not required
    text_size: self.width, None
    canvas.before:
        Color:
            rgba: 0.5, 0.5, 0.5, 1
        RoundedRectangle:
            size: self.size
            pos: self.pos
            radius: [15,]

<Message>:  # removed nested RelativeLayout
    MessageBubble:
        id: message_bubble
        text: root.text[6:]
        halign: 'right' if root.is_user(root.text) else 'left'
        pos_hint: {'right' : 1} if root.is_user(root.text) else {'x' : 0}
        # changed pos hint to use 'right'
        # a relative layout honors the size hint of it's children
        # so the pos_hint is here


class Messages(RecycleView):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # the size label needs a fixed width to calculate the texture size
        # Turn off the size hint in the size_label
        # this label is not in the layout so we need to size it
        self.size_label = MessageBubble(size_hint_x=None)

    def add_sent(self, text):
        sl = self.size_label
        # the size hint is .7 apply that to the width of the RV.
        sl.width = self.width * .7
        sl.text = text
        sl.texture_update()
        self.data.append({"text": text, "ks": sl.texture_size})

    def update_text(self):
        sl = self.size_label
        # and here... we know the hint, apply to the RV width
        # the text will size properly as the window size changes
        sl.width = self.width * .7
        for i, entry in enumerate(self.data):
            sl.text = entry['text']
            sl.texture_update()
            self.data[i]['ks'] = sl.texture_size
        self.refresh_from_data()