root/src/kits/shared/BarberPole.cpp
/*
 * Copyright 2017 Julian Harnath <julian.harnath@rwth-aachen.de>
 * All rights reserved. Distributed under the terms of the MIT license.
 */


#include "BarberPole.h"

#include <AutoLocker.h>
#include <ControlLook.h>
#include <Locker.h>
#include <ObjectList.h>
#include <Messenger.h>

#include <pthread.h>
#include <stdio.h>


// #pragma mark - MachineRoom


/*! The machine room spins all the barber poles.
    Keeps a list of all barber poles of this team and runs its own
    thread to invalidate them in regular intervals.
*/
class MachineRoom
{
private:
        enum {
                kSpinInterval = 20000 // us
        };

private:
        MachineRoom()
                :
                fMessengers(20)
        {
                fSpinLoopLock = create_sem(0, "BarberPole lock");
                fSpinLoopThread = spawn_thread(&MachineRoom::_StartSpinLoop,
                        "The Barber Machine", B_DISPLAY_PRIORITY, this);
                resume_thread(fSpinLoopThread);
        }

public:
        static void AttachBarberPole(BarberPole* pole)
        {
                _InitializeIfNeeded();
                sInstance->_Attach(pole);
        }

        static void DetachBarberPole(BarberPole* pole)
        {
                sInstance->_Detach(pole);
        }

private:
        static void _Initialize()
        {
                sInstance = new MachineRoom();
        }

        static status_t _StartSpinLoop(void* instance)
        {
                static_cast<MachineRoom*>(instance)->_SpinLoop();
                return B_OK;
        }

        static void _InitializeIfNeeded()
        {
                pthread_once(&sOnceControl, &MachineRoom::_Initialize);
        }

        void _Attach(BarberPole* pole)
        {
                AutoLocker<BLocker> locker(fLock);

                bool wasEmpty = fMessengers.IsEmpty();

                BMessenger* messenger = new BMessenger(pole);
                fMessengers.AddItem(messenger);

                if (wasEmpty)
                        release_sem(fSpinLoopLock);
        }

        void _Detach(BarberPole* pole)
        {
                AutoLocker<BLocker> locker(fLock);

                for (int32 i = 0; i < fMessengers.CountItems(); i++) {
                        BMessenger* messenger = fMessengers.ItemAt(i);
                        if (messenger->Target(NULL) == pole) {
                                fMessengers.RemoveItem(messenger, true);
                                break;
                        }
                }

                if (fMessengers.IsEmpty())
                        acquire_sem(fSpinLoopLock);
        }

        void _SpinLoop()
        {
                for (;;) {
                        AutoLocker<BLocker> locker(fLock);

                        for (int32 i = 0; i < fMessengers.CountItems(); i++) {
                                BMessenger* messenger = fMessengers.ItemAt(i);
                                messenger->SendMessage(BarberPole::kRefreshMessage);
                        }

                        locker.Unset();

                        acquire_sem(fSpinLoopLock);
                        release_sem(fSpinLoopLock);

                        snooze(kSpinInterval);
                }
        }

private:
        static MachineRoom*             sInstance;
        static pthread_once_t   sOnceControl;

        thread_id                               fSpinLoopThread;
        sem_id                                  fSpinLoopLock;

        BLocker                                 fLock;
        BObjectList<BMessenger, true> fMessengers;
};


MachineRoom* MachineRoom::sInstance = NULL;
pthread_once_t MachineRoom::sOnceControl = PTHREAD_ONCE_INIT;


// #pragma mark - BarberPole


BarberPole::BarberPole(const char* name)
        :
        BView(name, B_WILL_DRAW | B_FRAME_EVENTS | B_FULL_UPDATE_ON_RESIZE),
        fIsSpinning(false),
        fSpinSpeed(0.05),
        fColors(NULL),
        fNumColors(0),
        fScrollOffset(0.0),
        fStripeWidth(0.0),
        fNumStripes(0)
{
        // Default colors, chosen from system color scheme
        rgb_color defaultColors[2];
        rgb_color otherColor = tint_color(ui_color(B_STATUS_BAR_COLOR), 1.3);
        otherColor.alpha = 50;
        defaultColors[0] = otherColor;
        defaultColors[1] = B_TRANSPARENT_COLOR;
        SetColors(defaultColors, 2);
}


BarberPole::~BarberPole()
{
        Stop();
        delete[] fColors;
}


void
BarberPole::MessageReceived(BMessage* message)
{
        switch (message->what) {
                case kRefreshMessage:
                        _Spin();
                        break;

                default:
                        BView::MessageReceived(message);
                        break;
        }
}


void
BarberPole::Draw(BRect updateRect)
{
        if (fIsSpinning)
                _DrawSpin(updateRect);
        else
                _DrawNonSpin(updateRect);
}


void
BarberPole::_DrawSpin(BRect updateRect)
{
        // Draw color stripes
        float position = -fStripeWidth * (fNumColors + 0.5) + fScrollOffset;
                // Starting position: beginning of the second color cycle
                // The + 0.5 is so we start out without a partially visible stripe
                // on the left side (makes it simpler to loop)
        BRect bounds = Bounds();
        bounds.InsetBy(-2, -2);
        be_control_look->DrawStatusBar(this, bounds, updateRect,
                ui_color(B_PANEL_BACKGROUND_COLOR), ui_color(B_STATUS_BAR_COLOR),
                bounds.Width());
        SetDrawingMode(B_OP_ALPHA);
        uint32 colorIndex = 0;
        for (uint32 i = 0; i < fNumStripes; i++) {
                SetHighColor(fColors[colorIndex]);
                colorIndex++;
                if (colorIndex >= fNumColors)
                        colorIndex = 0;

                BRect stripeFrame = fStripe.Frame();
                fStripe.MapTo(stripeFrame,
                        stripeFrame.OffsetToCopy(position, 0.0));
                FillPolygon(&fStripe);

                position += fStripeWidth;
        }

        SetDrawingMode(B_OP_COPY);
        // Draw box around it
        bounds = Bounds();
        be_control_look->DrawBorder(this, bounds, updateRect,
                ui_color(B_PANEL_BACKGROUND_COLOR), B_PLAIN_BORDER);
}


/*! This will show something in the place of the spinner when there is no
    spinning going on.  The logic to render the striped background comes
    from the 'drivesetup' application.
*/

void
BarberPole::_DrawNonSpin(BRect updateRect)
{
        // approach copied from the DiskSetup application.
        static const pattern kStripes = { { 0xc7, 0x8f, 0x1f, 0x3e, 0x7c,
                0xf8, 0xf1, 0xe3 } };
        BRect bounds = Bounds();
        SetHighUIColor(B_PANEL_BACKGROUND_COLOR, B_DARKEN_1_TINT);
        SetLowUIColor(B_PANEL_BACKGROUND_COLOR);
        FillRect(bounds, kStripes);
        be_control_look->DrawBorder(this, bounds, updateRect,
                ui_color(B_PANEL_BACKGROUND_COLOR), B_PLAIN_BORDER);
}


void
BarberPole::FrameResized(float width, float height)
{
        // Choose stripe width so that at least 2 full stripes fit into the view,
        // but with a minimum of 5px. Larger views get wider stripes, but they
        // grow slower than the view and are capped to a maximum of 200px.
        fStripeWidth = (width / 4) + 5;
        if (fStripeWidth > 200)
                fStripeWidth = 200;

        BPoint stripePoints[4];
        stripePoints[0].Set(fStripeWidth * 0.5, 0.0); // top left
        stripePoints[1].Set(fStripeWidth * 1.5, 0.0); // top right
        stripePoints[2].Set(fStripeWidth, height);    // bottom right
        stripePoints[3].Set(0.0, height);             // bottom left

        fStripe = BPolygon(stripePoints, 4);

        fNumStripes = (int32)ceilf((width) / fStripeWidth) + 1 + fNumColors;
                // Number of color stripes drawn in total for the barber pole, the
                // user-visible part is a "window" onto the complete pole. We need
                // as many stripes as are visible, an extra one on the right side
                // (will be partially visible, that's the + 1); and then a whole color
                // cycle of strips extra which we scroll into until we loop.
                //
                // Example with 3 colors and a visible area of 2*fStripeWidth (which means
                // that 2 will be fully visible, and a third one partially):
                //               ........
                //   X___________v______v___
                //  / 1 / 2 / 3 / 1 / 2 / 3 /
                //  `````````````````````````
                // Pole is scrolled to the right into the visible region, which is marked
                // between the two 'v'. Once the left edge of the visible area reaches
                // point X, we can jump back to the initial region position.
}


BSize
BarberPole::MinSize()
{
        BSize result = BView::MinSize();

        if (result.width < 50)
                result.SetWidth(50);

        if (result.height < 5)
                result.SetHeight(5);

        return result;
}


void
BarberPole::Start()
{
        if (fIsSpinning)
                return;
        MachineRoom::AttachBarberPole(this);
        fIsSpinning = true;
}


void
BarberPole::Stop()
{
        if (!fIsSpinning)
                return;
        MachineRoom::DetachBarberPole(this);
        fIsSpinning = false;
        Invalidate();
}


void
BarberPole::SetSpinSpeed(float speed)
{
        if (speed > 1.0f)
                speed = 1.0f;
        if (speed < -1.0f)
                speed = -1.0f;
        fSpinSpeed = speed;
}


void
BarberPole::SetColors(const rgb_color* colors, uint32 numColors)
{
        delete[] fColors;
        rgb_color* colorsCopy = new rgb_color[numColors];
        for (uint32 i = 0; i < numColors; i++)
                colorsCopy[i] = colors[i];

        fColors = colorsCopy;
        fNumColors = numColors;
}


void
BarberPole::_Spin()
{
        fScrollOffset += fStripeWidth / (1.0f / fSpinSpeed);
        if (fScrollOffset >= fStripeWidth * fNumColors) {
                // Cycle completed, jump back to where we started
                fScrollOffset = 0;
        }
        Invalidate();
        //Parent()->Invalidate();
}